1. Overview
1.概述
Generally, when we need to validate user input, Spring MVC offers standard predefined validators.
一般来说,当我们需要验证用户输入时,Spring MVC提供标准的预定义验证器。
However, when we need to validate a more particular type of input, we have the ability to create our own custom validation logic.
然而,当我们需要验证一个更特殊的输入类型时,我们有能力创建我们自己的自定义验证逻辑。
In this tutorial, we’ll do just that; we’ll create a custom validator to validate a form with a phone number field, and then we’ll show a custom validator for multiple fields.
在本教程中,我们将做到这一点;我们将创建一个自定义验证器来验证一个带有电话号码字段的表单,然后我们将展示一个用于多个字段的自定义验证器。
This tutorial focuses on Spring MVC. Our article entitled Validation in Spring Boot describes how to create custom validations in Spring Boot.
本教程的重点是Spring MVC。我们题为Spring Boot中的验证的文章介绍了如何在Spring Boot中创建自定义验证。
2. Setup
2.设置
To benefit from the API, we’ll add the dependency to our pom.xml file:
为了从API中获益,我们将把这个依赖性添加到我们的pom.xml文件中。
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.10.Final</version>
</dependency>
The latest version of the dependency can be checked here.
最新版本的依赖关系可以在这里检查。
If we’re using Spring Boot, then we can only add the spring-boot-starter-web, which will bring in the hibernate-validator dependency also.
如果我们使用Spring Boot,那么我们只能添加spring-boot-starter-web,这也会带来hibernate-validator依赖。
3. Custom Validation
3.自定义验证
Creating a custom validator entails rolling out our own annotation and using it in our model to enforce the validation rules.
创建一个自定义验证器需要推出我们自己的注解,并在我们的模型中使用它来执行验证规则。
So let’s create our custom validator, which checks phone numbers. The phone number must be a number with at least eight digits, but no more than 11 digits.
因此,让我们创建我们的自定义验证器,它检查电话号码。电话号码必须是一个至少有8位数字,但不超过11位的数字。
4. The New Annotation
4.新的注释
Let’s create a new @interface to define our annotation:
让我们创建一个新的@interface来定义我们的注释。
@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 {};
}
With the @Constraint annotation, we defined the class that is going to validate our field. The message() is the error message that is showed in the user interface. Finally, the additional code is mostly boilerplate code to conform to the Spring standards.
通过@Constraint 注解,我们定义了将验证我们字段的类。message() 是显示在用户界面上的错误信息。最后,额外的代码大部分是模板代码,以符合Spring的标准。
5. Creating a Validator
5.创建一个验证器
Now let’s create a validator class that enforces the rules of our validation:
现在让我们创建一个验证器类,执行我们的验证规则。
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);
}
}
The validation class implements the ConstraintValidator interface, and must also implement the isValid method; it’s in this method that we defined our validation rules.
验证类实现了ConstraintValidator接口,而且还必须实现isValid方法;我们正是在这个方法中定义了我们的验证规则。
Naturally, we’re going with a simple validation rule here in order to show how the validator works.
当然,我们在这里使用一个简单的验证规则,以展示验证器的工作原理。
ConstraintValidator defines the logic to validate a given constraint for a given object. Implementations must comply with the following restrictions:
ConstraintValidator定义了为给定对象验证给定约束的逻辑。实现必须遵守以下限制。
- the object must resolve to a non-parametrized type
- generic parameters of the object must be unbounded wildcard types
6. Applying Validation Annotation
6.应用验证性注解
In our case, we created a simple class with one field to apply the validation rules. Here we’re setting up our annotated field to be validated:
在我们的案例中,我们创建了一个有一个字段的简单类来应用验证规则。在这里,我们正在设置我们的注释字段来进行验证。
@ContactNumberConstraint
private String phone;
We defined a string field and annotated it with our custom annotation, @ContactNumberConstraint. In our controller, we created our mappings and handled any errors:
我们定义了一个字符串字段,并用我们的自定义注解@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";
}
}
We defined this simple controller that has a single JSP page, and used the submitForm method to enforce the validation of our phone number.
我们定义了这个简单的控制器,它有一个JSP页面,并使用submitForm方法来强制验证我们的电话号码。
7. The View
7.风景
Our view is a basic JSP page with a form that has a single field. When the user submits the form, the field gets validated by our custom validator and redirects to the same page with a message of successful or failed validation:
我们的视图是一个基本的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. Tests
8.测试
Now let’s test our controller to check if it’s giving us the appropriate response and view:
现在让我们测试一下我们的控制器,看看它是否给了我们适当的响应和视图。
@Test
public void givenPhonePageUri_whenMockMvc_thenReturnsPhonePage(){
this.mockMvc.
perform(get("/validatePhone")).andExpect(view().name("phoneHome"));
}
Let’s also test that our field is validated based on user input:
让我们也测试一下,我们的字段是否根据用户的输入进行了验证。
@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());
}
In the test, we’re providing a user with the input of “123,” and as we expected, everything’s working and we’re seeing the error on the client side.
在测试中,我们为用户提供了 “123 “的输入,正如我们所期望的,一切都在工作,我们在客户端看到了错误。
9. Custom Class Level Validation
9.自定义类级验证
A custom validation annotation can also be defined at the class level to validate more than one attribute of the class.
也可以在类的层面上定义一个自定义的验证注解,以验证类的一个以上的属性。
A common use case for this scenario is verifying if two fields of a class have matching values.
这种情况的一个常见用例是验证一个类的两个字段是否有匹配的值。
9.1. Creating the Annotation
9.1.创建注释
Let’s add a new annotation called FieldsValueMatch that can be later applied to a class. The annotation will have two parameters, field and fieldMatch, that represent the names of the fields to compare:
让我们添加一个名为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();
}
}
We can see our custom annotation also contains a List sub-interface for defining multiple FieldsValueMatch annotations on a class.
我们可以看到我们的自定义注解还包含一个List子接口,用于在一个类上定义多个FieldsValueMatch注解。
9.2. Creating the Validator
9.2.创建验证器
Next we need to add the FieldsValueMatchValidator class that will contain the actual validation logic:
接下来我们需要添加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;
}
}
}
The isValid() method retrieves the values of the two fields and checks if they are equal.
isValid()方法检索两个字段的值并检查它们是否相等。
9.3. Applying the Annotation
9.3.应用注释
Let’s create a NewUserForm model class intended for the data required for user registration. It will have two email and password attributes, along with two verifyEmail and verifyPassword attributes to re-enter the two values.
让我们创建一个NewUserForm模型类,用于用户注册所需的数据。它将有两个email和password属性,以及两个verifyEmail和verifyPassword属性来重新输入这两个值。
Since we have two fields to check against their corresponding matching fields, let’s add two @FieldsValueMatch annotations on the NewUserForm class, one for email values, and one for password values:
由于我们有两个字段要与相应的匹配字段进行检查,让我们在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
}
To validate the model in Spring MVC, let’s create a controller with a /user POST mapping that receives a NewUserForm object annotated with @Valid and verifies whether there are any validation errors:
为了在Spring MVC中验证模型,让我们创建一个带有/user POST映射的控制器,该控制器接收带有@Valid注释的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. Testing the Annotation
9.4.测试注释
To verify our custom class-level annotation, let’s write a JUnit test that sends matching information to the /user endpoint, then verifies that the response contains no errors:
为了验证我们的自定义类级注解,让我们写一个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", "john@yahoo.com")
.param("verifyEmail", "john@yahoo.com")
.param("password", "pass")
.param("verifyPassword", "pass"))
.andExpect(model().errorCount(0))
.andExpect(status().isOk());
}
}
Then we’ll also add a JUnit test that sends non-matching information to the /user endpoint and asserts that the result will contain two errors:
然后我们还要添加一个JUnit测试,向/user端点发送非匹配信息,并断言结果将包含两个错误。
@Test
public void givenNotMatchingEmailPassword_whenPostNewUserForm_thenOk()
throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders
.post("/user")
.accept(MediaType.TEXT_HTML)
.param("email", "john@yahoo.com")
.param("verifyEmail", "john@yahoo.commmm")
.param("password", "pass")
.param("verifyPassword", "passsss"))
.andExpect(model().errorCount(2))
.andExpect(status().isOk());
}
10. Summary
10.总结
In this brief article, we learned how to create custom validators to verify a field or class, and then wire them into Spring MVC.
在这篇简短的文章中,我们学习了如何创建自定义验证器来验证一个字段或类,然后将它们接入Spring MVC。
As always, the code from this article is available over on Github.
像往常一样,本文的代码可在Github上获得。