1. 概述
通常,當我們需要驗證用户輸入時,Spring MVC 提供標準預定義的驗證器。
但是,當我們需要驗證更特定類型的輸入時,我們有能力創建自己的自定義驗證邏輯。
在本教程中,我們將做到這一點;我們將創建一個自定義驗證器來驗證包含電話號碼字段的表單,然後我們將展示自定義驗證器對多個字段的用法。
本教程專注於 Spring MVC。我們的文章《Spring Boot 中的驗證》描述瞭如何在 Spring Boot 中創建自定義驗證。
2. 設置
為了充分利用 API,我們將添加依賴項到我們的 <em pom.xml</em> 文件中:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.1.Final</version>
</dependency>
最新版本的依賴項可以在這裏進行檢查:這裏。
如果使用 Spring Boot,則添加 spring-boot-starter-validation,它也會引入 hibernate-validator 依賴項。
3. 自定義驗證
創建自定義驗證器意味着自己定義標註並將其用於模型中,以強制執行驗證規則。
讓我們創建一個 自定義驗證器,用於檢查電話號碼。電話號碼必須是一個至少包含八位數字,最多包含 eleven 位數字的數字。
4. 新註解
讓我們創建一個新的 `@interface</em/> 來定義我們的註解:
@Documented
@Constraint(validatedBy = ContactNumberValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ContactNumberConstraint {
String message() default "Invalid phone number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}使用 @Constraint 註解,我們定義了用於驗證字段的類。message() 是在用户界面上顯示的錯誤消息。最後,大部分代碼是用於符合 Spring 標準的樣板代碼。
5. 創建驗證器
現在,讓我們創建一個驗證器類,該類會強制執行我們定義的驗證規則:
public class ContactNumberValidator implements
ConstraintValidator<ContactNumberConstraint, String> {
@Override
public void initialize(ContactNumberConstraint contactNumber) {
}
@Override
public boolean isValid(String contactField,
ConstraintValidatorContext cxt) {
return contactField != null && contactField.matches("[0-9]+")
&& (contactField.length() > 8) && (contactField.length() < 14);
}
}驗證類實現了 ConstraintValidator 接口,並且必須也實現 isValid 方法;在該方法中,我們定義了我們的驗證規則。
自然地,我們在這裏採用一個簡單的驗證規則,以便展示驗證器的工作原理。
ConstraintValidator 定義了為給定對象驗證給定約束的邏輯。 實現必須符合以下限制:
- 對象必須解析為非參數化的類型
- 對象中的泛型參數必須是未限定通配符類型
6. 應用驗證標註
在我們的例子中,我們創建了一個簡單的類,其中包含一個字段來應用驗證規則。這裏我們正在設置我們的標註字段進行驗證:
@ContactNumberConstraint
private String phone;我們定義了一個字符串字段,並使用自定義註解 @ContactNumberConstraint 進行了標註。在我們的控制器中,我們創建了映射,並處理了任何錯誤:
@Controller
public class ValidatedPhoneController {
@GetMapping("/validatePhone")
public String loadFormPage(Model m) {
m.addAttribute("validatedPhone", new ValidatedPhone());
return "phoneHome";
}
@PostMapping("/addValidatePhone")
public String submitForm(@Valid ValidatedPhone validatedPhone,
BindingResult result, Model m) {
if(result.hasErrors()) {
return "phoneHome";
}
m.addAttribute("message", "Successfully saved phone: "
+ validatedPhone.toString());
return "phoneHome";
}
}我們定義了這個簡單的控制器,它包含一個 JSP 頁面,並使用 submitForm 方法來驗證我們的電話號碼。
7. 視圖
我們的視圖是一個基本的 JSP 頁面,包含一個表單,該表單只有一個字段。當用户提交表單時,字段將由我們的自定義驗證器進行驗證,並重定向到同一頁面,帶有成功或驗證失敗的消息:
<form:form
action="/${pageContext.request.contextPath}/addValidatePhone"
modelAttribute="validatedPhone">
<label for="phoneInput">Phone: </label>
<form:input path="phone" id="phoneInput" />
<form:errors path="phone" cssClass="error" />
<input type="submit" value="Submit" />
</form:form>
8. 測試
現在讓我們測試我們的控制器,以檢查它是否返回了適當的響應和視圖:
@Test
public void givenPhonePageUri_whenMockMvc_thenReturnsPhonePage(){
this.mockMvc.
perform(get("/validatePhone")).andExpect(view().name("phoneHome"));
}讓我們也測試一下我們的字段是否基於用户輸入進行驗證:
@Test
public void
givenPhoneURIWithPostAndFormData_whenMockMVC_thenVerifyErrorResponse() {
this.mockMvc.perform(MockMvcRequestBuilders.post("/addValidatePhone").
accept(MediaType.TEXT_HTML).
param("phoneInput", "123")).
andExpect(model().attributeHasFieldErrorCode(
"validatedPhone","phone","ContactNumberConstraint")).
andExpect(view().name("phoneHome")).
andExpect(status().isOk()).
andDo(print());
}在測試中,我們向用户提供輸入“123”,正如我們所期望的,一切正常,並且我們觀察到錯誤發生在客户端。
9. 自定義類級別驗證
還可以定義自定義驗證註解,用於驗證類的多個屬性。
這種場景的常見用例是驗證類中兩個字段是否具有匹配的值。
9.1. 創建標註
讓我們添加一個名為 FieldsValueMatch 的新標註,該標註可稍後應用於類。該標註將具有兩個參數,field 和 fieldMatch,它們分別代表用於比較的字段名稱:
@Constraint(validatedBy = FieldsValueMatchValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldsValueMatch {
String message() default "Fields values don't match!";
String field();
String fieldMatch();
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@interface List {
FieldsValueMatch[] value();
}
}我們還可以看到我們的自定義標註也包含一個 List 子接口,用於定義類上的多個 FieldsValueMatch 標註。
9.2. 創建驗證器
接下來,我們需要添加 FieldsValueMatchValidator 類,該類將包含實際的驗證邏輯:
public class FieldsValueMatchValidator
implements ConstraintValidator<FieldsValueMatch, Object> {
private String field;
private String fieldMatch;
public void initialize(FieldsValueMatch constraintAnnotation) {
this.field = constraintAnnotation.field();
this.fieldMatch = constraintAnnotation.fieldMatch();
}
public boolean isValid(Object value,
ConstraintValidatorContext context) {
Object fieldValue = new BeanWrapperImpl(value)
.getPropertyValue(field);
Object fieldMatchValue = new BeanWrapperImpl(value)
.getPropertyValue(fieldMatch);
if (fieldValue != null) {
return fieldValue.equals(fieldMatchValue);
} else {
return fieldMatchValue == null;
}
}
}isValid() 方法檢索兩個字段的值並檢查它們是否相等。
9.3. 應用標註
讓我們創建一個名為 NewUserForm 的模型類,用於用户註冊所需的數據。它將包含兩個 email 和 password 屬性,以及兩個 verifyEmail 和 verifyPassword 屬性,用於重新輸入這兩個值。
由於我們有兩個字段需要與對應的值進行驗證,因此我們在 NewUserForm 類上添加了兩個 @FieldsValueMatch 註解,一個用於 email 值,另一個用於 password 值:
@FieldsValueMatch.List({
@FieldsValueMatch(
field = "password",
fieldMatch = "verifyPassword",
message = "Passwords do not match!"
),
@FieldsValueMatch(
field = "email",
fieldMatch = "verifyEmail",
message = "Email addresses do not match!"
)
})
public class NewUserForm {
private String email;
private String verifyEmail;
private String password;
private String verifyPassword;
// standard constructor, getters, setters
}為了在 Spring MVC 中驗證模型,我們創建一個帶有 /user POST 映射的控制器,該控制器接收一個帶有 NewUserForm 對象的請求,並驗證是否存在任何驗證錯誤:
@Controller
public class NewUserController {
@GetMapping("/user")
public String loadFormPage(Model model) {
model.addAttribute("newUserForm", new NewUserForm());
return "userHome";
}
@PostMapping("/user")
public String submitForm(@Valid NewUserForm newUserForm,
BindingResult result, Model model) {
if (result.hasErrors()) {
return "userHome";
}
model.addAttribute("message", "Valid form");
return "userHome";
}
}9.4. 驗證標註
為了驗證我們自定義的類級別標註,我們編寫一個 JUnit 測試,將匹配的信息發送到 /user 端點,然後驗證響應中是否包含任何錯誤:
public class ClassValidationMvcTest {
private MockMvc mockMvc;
@Before
public void setup(){
this.mockMvc = MockMvcBuilders
.standaloneSetup(new NewUserController()).build();
}
@Test
public void givenMatchingEmailPassword_whenPostNewUserForm_thenOk()
throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders
.post("/user")
.accept(MediaType.TEXT_HTML).
.param("email", "[email protected]")
.param("verifyEmail", "[email protected]")
.param("password", "pass")
.param("verifyPassword", "pass"))
.andExpect(model().errorCount(0))
.andExpect(status().isOk());
}
}然後,我們還將添加一個使用 JUnit 的測試,該測試將發送不匹配的信息到 /user 端點,並斷言結果將包含兩個錯誤:
@Test
public void givenNotMatchingEmailPassword_whenPostNewUserForm_thenOk()
throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders
.post("/user")
.accept(MediaType.TEXT_HTML)
.param("email", "[email protected]")
.param("verifyEmail", "[email protected]")
.param("password", "pass")
.param("verifyPassword", "passsss"))
.andExpect(model().errorCount(2))
.andExpect(status().isOk());
}10. 總結
在本文中,我們學習瞭如何創建自定義驗證器來驗證字段或類,並將它們連接到 Spring MVC。