JSR-303 Specification
Before the program performs data processing, it is something we must consider. Discovering data errors as early as possible can not only prevent the error from spreading to the core business logic, but also this error is very obvious and easy to detect and resolve.
The JSR303 specification (Bean Validation specification) defines the corresponding metadata model and API for JavaBean verification. In the application, you can ensure the correctness of the data model (JavaBean) by using Bean Validation or your own defined constraint, such as @NotNull, @Max, @ZipCode. constraint can be attached to fields, getter methods, classes, or interfaces. For some specific needs, users can easily develop customized constraints. Bean Validation is a runtime data verification framework, and the verified error message will be returned immediately after verification.
About JSR 303 – Bean Validation specification, please refer toOfficial website
For the JSR 303 specification, Hibernate Validator provides reference implementations of it. Hibernate Validator provides all the built-in constraints in the JSR 303 specification, and there are some additional constraints. If you want to learn more about Hibernate Validator, please check it outOfficial website。
Constraint | Details |
---|---|
@AssertFalse | The annotated element must be false |
@AssertTrue | Same as @AssertFalse |
@DecimalMax | The commented element must be a number, and its value must be less than or equal to the specified maximum value. |
@DecimalMin | Same as DecimalMax |
@Digits | The annotated element must be a number within an acceptable range |
As the name implies | |
@Future | Future dates |
@FutureOrPresent | Now or in the future |
@Max | The commented element must be a number, and its value must be less than or equal to the specified maximum value. |
@Min | The commented element must be a number, and its value must be greater than or equal to the specified minimum value. |
@Negative | The annotated element must be a strictly negative number (0 is an invalid value) |
@NegativeOrZero | The annotated element must be a strictly negative number (including 0) |
@NotBlank | same |
@NotEmpty | same |
@NotNull | Can't be Null |
@Null | The element is Null |
@Past | The commented element must be a past date |
@PastOrPresent | Past and present |
@Pattern | The annotated element must comply with the specified regular expression |
@Positive | The annotated element must have a strict positive number (0 is an invalid value) |
@PositiveOrZero | The annotated element must have a strict positive number (including 0) |
@Szie | The annotated element size must be between the specified boundary (including) |
Hibernate Validator Attached constraint
Constraint | Details |
---|---|
The annotated element must be an email address | |
@Length | The commented string must be within the specified range |
@NotEmpty | The commented string must be non-empty |
@Range | The annotated element must be within the appropriate range |
CreditCardNumber | The annotated element must comply with the credit card format |
The Constraints attached to different versions of Hibernate Validator may be different, and you also need to check the version you use. Constraint provided by Hibernate inUnder this bag.
A constraint is usually composed of annotation and corresponding constraint validator, which are a one-to-many relationship. That is to say, there can be multiple constraint validators corresponding to an annotation. At runtime, the Bean Validation framework itself selects the appropriate constraint validator to verify the data based on the type of the annotated element.
Sometimes, some more complex constraints are needed in user applications. Bean Validation provides a mechanism to extend constraints. There are two ways to achieve it, one is to combine existing constraints to generate a more complex constraint, and the other is to develop a brand new constraint.
Use Spring Boot for data verification
Spring Validation secondary encapsulation of hibernate validation allows us to use the data verification function more conveniently. Here we use Spring Boot to reference the verification function.
If you use Spring Boot version less than 2., spring-boot-starter-web will automatically introduce the hibernate-validator dependency. If Spring Boot version is greater than 2., you need to manually introduce dependencies:
<dependency> <groupId></groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.</version> </dependency>
Direct parameter verification
Sometimes the interface has fewer parameters, only one is alive with two parameters. At this time, there is no need to define a DTO to receive parameters, and you can directly receive parameters.
@Validated @RestController @RequestMapping("/user") public class UserController { private static Logger logger = (); @GetMapping("/getUser") @ResponseBody // Note: If you want to use @NotNull annotation verification in the parameters, you must add @Validated to the class; public UserDTO getUser(@NotNull(message = "userId cannot be empty") Integer userId){ ("userId:[{}]",userId); UserDTO res = new UserDTO(); (userId); ("The Road to Freedom of Programmers"); (8); return res; } }
The following is the unified exception handling class
@RestControllerAdvice public class GlobalExceptionHandler { private static final Logger logger = (); @ExceptionHandler(value = ) public Response handle1(ConstraintViolationException ex){ StringBuilder msg = new StringBuilder(); Set<ConstraintViolation<?>> constraintViolations = (); for (ConstraintViolation<?> constraintViolation : constraintViolations) { PathImpl pathImpl = (PathImpl) (); String paramName = ().getName(); String message = (); ("[").append(message).append("]"); } ((),ex); // Note: The Response class must have a get and set method, otherwise an error will be reported. return new Response(RCode.PARAM_INVALID.getCode(),()); } @ExceptionHandler(value = ) public Response handle1(Exception ex){ ((),ex); return new Response((),()); } }
Call result
# There is no userId passed hereGET http://127.0.0.1:9999/user/getUser HTTP/1.1 200 Content-Type: application/json Transfer-Encoding: chunked Date: Sat, 14 Nov 2020 07:35:44 GMT Keep-Alive: timeout=60 Connection: keep-alive { "rtnCode": "1000", "rtnMsg": "[userId cannot be empty]" }
Entity DTO verification
Define a DTO
import ; import ; public class UserDTO { private Integer userId; @NotEmpty(message = "The name cannot be empty") private String name; @Range(min = 18,max = 50,message = "Age must be between 18 and 50") private Integer age; //Omit the get and set methods}
Use @Validated for verification when receiving parameters
@PostMapping("/saveUser") @ResponseBody //Note: If the parameter in the method is an object type, you must add @Validated in front of the parameter object.public Response<UserDTO> getUser(@Validated @RequestBody UserDTO userDTO){ (100); Response response = (); (userDTO); return response; }
Unified exception handling
@ExceptionHandler(value = ) public Response handle2(MethodArgumentNotValidException ex){ BindingResult bindingResult = (); if(bindingResult!=null){ if(()){ FieldError fieldError = (); String field = (); String defaultMessage = (); ((),ex); return new Response(RCode.PARAM_INVALID.getCode(),field+":"+defaultMessage); }else { ((),ex); return new Response((),()); } }else { ((),ex); return new Response((),()); } }
Call result
### Create a user
POST http://127.0.0.1:9999/user/saveUser
Content-Type: application/json{
"name1": "Programmer's Road to Freedom",
"age": "18"
}# Below is the return result
{
"rtnCode": "1000",
"rtnMsg": "The name cannot be empty"
}
Verify Service layer method parameters
I don’t like this verification method very much. In half of the cases, the parameters that call the service layer method need to be verified in the controller layer, and there is no need to be verified again. This feature is listed here, just want to say that Spring also supports this.
@Validated @Service public class ValidatorService { private static final Logger logger = (); public String show(@NotNull(message = "Can't be empty") @Min(value = 18, message = "Minimum 18") String age) { ("age = {}", age); return age; } }
Group verification
Sometimes, different verification rules for DTO are required for different interfaces. The UserDTO above is still the column. Another interface may not need to limit the age between 18 and 50, it only needs to be greater than 18.
In this way, the above verification rules will not apply. Group verification is to solve this problem. Different groups adopt different verification strategies for the same DTO.
public class UserDTO { public interface Default { } public interface Group1 { } private Integer userId; //Note: After adding groups attributes to the @Validated annotation, the verification rule without group attributes in DTO will be invalid @NotEmpty(message = "The name cannot be empty",groups = ) private String name; //Note: After adding the groups attribute, the group attribute must be added to the @Validated annotation before the verification rules can take effect, otherwise the following verification limit will be invalid @Range(min = 18, max = 50, message = "Age must be between 18 and 50",groups = ) @Range(min = 17, message = "Age must be greater than 17", groups = ) private Integer age; }
How to use
@PostMapping("/saveUserGroup") @ResponseBody //Note: If the parameter in the method is an object type, you must add @Validated in front of the parameter object.//Check group verification, age satisfaction is greater than 17public Response<UserDTO> saveUserGroup(@Validated(value = {UserDTO.}) @RequestBody UserDTO userDTO){ (100); Response response = (); (userDTO); return response; }
Use Group1 packet for verification, because in DTO, Group1 packet does not have a verification on the name attribute, so this verification will not take effect.
The advantage of group verification is that different verification rules can be set for the same DTO, but the disadvantage is that for each new verification group, the verification rules for each attribute under this group need to be reset.
Group verification also has a sequential verification function.
Consider a scenario: a bean has 1 attribute (if it is attrA), and 3 constraints are added to this attribute (if it is @NotNull, @NotEmpty, @NotBlank). By default, validation-api's check order for these 3 constraints is random. In other words, it may first verify @NotNull, then @NotEmpty, and finally @NotBlank, or it may verify @NotBlank, then @NotEmpty, and finally @NotNull.
Then, if our requirement is to first verify @NotNull, then verify @NotBlank, and finally verify @NotEmpty. The @GroupSequence annotation can implement this function.
public class GroupSequenceDemoForm { @NotBlank(message = "Contains at least one non-empty character", groups = {}) @Size(min = 11, max = 11, message = "The length must be 11", groups = {}) private String demoAttr; public interface First { } public interface Second { } @GroupSequence(value = {, }) public interface GroupOrderedOne { // First calculate the constraints belonging to the First group, and then calculate the constraints belonging to the Second group } @GroupSequence(value = {, }) public interface GroupOrderedTwo { // First calculate the constraints belonging to the Second group, and then calculate the constraints belonging to the First group } }
How to use
// First calculate the constraints belonging to the First group, and then calculate the constraints belonging to the Second group@Validated(value = {}) @RequestBody GroupSequenceDemoForm form
Nested verification
In the previous example, the fields in the DTO class are all types such as basic data types and String.
However, in actual scenarios, it is possible that a certain field is also an object. If we need to verify the data in this object, we can use nested verification.
If there is also a Job object in UserDTO, such as the following structure. It should be noted that the @Valid annotation must be added to the verification of the job class.
public class UserDTO1 { private Integer userId; @NotEmpty private String name; @NotNull private Integer age; @Valid @NotNull private Job job; public Integer getUserId() { return userId; } public void setUserId(Integer userId) { = userId; } public String getName() { return name; } public void setName(String name) { = name; } public Integer getAge() { return age; } public void setAge(Integer age) { = age; } public Job getJob() { return job; } public void setJob(Job job) { = job; } /** * This must be set to a static internal class */ static class Job { @NotEmpty private String jobType; @DecimalMax(value = "1000.99") private Double salary; public String getJobType() { return jobType; } public void setJobType(String jobType) { = jobType; } public Double getSalary() { return salary; } public void setSalary(Double salary) { = salary; } } }
How to use
@PostMapping("/saveUserWithJob") @ResponseBody public Response<UserDTO1> saveUserWithJob(@Validated @RequestBody UserDTO1 userDTO){ (100); Response response = (); (userDTO); return response; }
Test results
POST http://127.0.0.1:9999/user/saveUserWithJob
Content-Type: application/json{
"name": "The Road to Freedom of Programmers",
"age": "16",
"job": {
"jobType": "1",
"salary": "9999.99"
}
}{
"rtnCode": "1000",
"rtnMsg": ": Must be less than or equal to 1000.99"
}
Nested verification can be used in conjunction with group verification. Also, nested set verification will verify every item in the set, for example, the List field will verify every job object in this list. This point
The difference between @Valid and @Validated is discussed in detail below.
Collection verification
If the request body directly passes the json array to the background and hopes to perform parameter verification on each item in the array. At this time, if we directly use the list or set to receive data, the parameter verification will not take effect! We can use a custom list collection to receive parameters:
Wrapping List type and declaring @Valid annotation
public class ValidationList<T> implements List<T> { // @Delegate is a lombok annotation // In the implementation of the List interface, a series of methods are required. Using this annotation can be delegated to the ArrayList implementation // @Delegate @Valid public List list = new ArrayList<>(); @Override public int size() { return (); } @Override public boolean isEmpty() { return (); } @Override public boolean contains(Object o) { return (o); } //.... The following omits a series of List interface methods, which are actually called ArrayList methods}
Calling methods
@PostMapping("/batchSaveUser") @ResponseBody public Response batchSaveUser(@Validated(value = ) @RequestBody ValidationList<UserDTO> userDTOs){ return (); }
Call result
Caused by: : Invalid property 'list[1]' of bean class []: Bean property 'list[1]' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
at (:622) ~[spring-beans-5.2.:5.2.]
at (:839) ~[spring-beans-5.2.:5.2.]
at (:816) ~[spring-beans-5.2.:5.2.]
at (:610) ~[spring-beans-5.2.:5.2.]
A NotReadablePropertyException exception will be thrown, and this exception needs to be handled uniformly. I won't post the code here.
Custom checker
Customizing a verification device in Spring is very simple, and it is done in two steps.
Custom constraint annotations
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {}) public @interface EncryptId { // Default error message String message() default "Encryption id format error"; // Grouping Class[] groups() default {}; // Load Class[] payload() default {}; }
Implement the ConstraintValidator interface to write a constraint verification device
public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> { private static final Pattern PATTERN = ("^[a-f\\d]{32,256}$"); @Override public boolean isValid(String value, ConstraintValidatorContext context) { // Verification is performed only if it is not null if (value != null) { Matcher matcher = (value); return (); } return true; } }
Programming verification
The above examples are all based on annotations to implement automatic verification, and in some cases we may want to call verification programmatically. This time you can inject it
object, then call its api.
@Autowired private globalValidator; // Programming verification@PostMapping("/saveWithCodingValidate") public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) { Set<constraintviolation> validate = (userDTO, ); // If the verification is passed, validate is empty; otherwise, validate contains items that are not validated. if (()) { // Only after the verification is passed will the business logic process be performed } else { for (ConstraintViolation userDTOConstraintViolation : validate) { // Check failed, do other logic (userDTOConstraintViolation); } } return (); }
Fail Fast configuration
Spring Validation will verify all fields by default before throwing an exception. You can enable Fali Fast mode through some simple configurations and return immediately once the verification fails.
@Bean public Validator validator() { ValidatorFactory validatorFactory = () .configure() // Fast failure mode .failFast(true) .buildValidatorFactory(); return (); }
Internationalization of verification information
Spring's verification function can return very friendly verification information prompts, and this information supports internationalization.
This function is not commonly used for the time being, please refer to this article for details.article
The difference between @Validated and @Valid
First of all, @Validated and @Valid can implement basic verification functions, that is, if you want to verify whether a parameter is empty and whether the length meets the requirements, you can use any annotation.
However, these two annotations are different in terms of functions such as grouping, where the annotation works, and nesting verification. The main differences between these two annotations are listed below.
- @Valid annotation is an annotation of the JSR303 specification, and @Validated annotation is an annotation that comes with Spring framework;
- @Valid does not have group verification function, @Validate has group verification function;
- @Valid can be used on methods, constructors, method parameters and member properties (fields), and @Validated can be used on types, methods and method parameters. However, it cannot be used on member attributes (fields). Whether the two can be used on member attributes (fields) directly affects whether nested verification functions can be provided;
- @Valid added to member attributes can be nested to verify member attributes, while @Validate cannot be added to member attributes, so this function does not exist.
Here we explain what nested verification is.
We now have an entity called Item:
public class Item { @NotNull(message = "id cannot be empty") @Min(value = 1, message = "id must be a positive integer") private Long id; @NotNull(message = "props cannot be empty") @Size(min = 1, message = "At least one attribute") private List<Prop> props; }
Item has many attributes, including: pid, vid, pidName and vidName, as shown below:
public class Prop { @NotNull(message = "pid cannot be empty") @Min(value = 1, message = "pid must be a positive integer") private Long pid; @NotNull(message = "vid cannot be empty") @Min(value = 1, message = "vid must be a positive integer") private Long vid; @NotBlank(message = "pidName cannot be empty") private String pidName; @NotBlank(message = "vidName cannot be empty") private String vidName; }
The attribute entity also has its own verification mechanism, such as pid and vid cannot be empty, pidName and vidName cannot be empty, etc.
Now we have an ItemController accepts an entry parameter of an Item and want to verify the Item, as shown below:
@RestController public class ItemController { @RequestMapping("/item/add") public void addItem(@Validated Item item, BindingResult bindingResult) { doSomething(); } }
In the above figure, if the props attribute of the Item entity is not annotated, only @NotNull and @Size, whether the parameter is @Validated or @Valid validation, the Spring Validation framework will only perform non-null and quantity verification on the Item id and props, and will not perform field verification on the Prop entity in the props field. That is, before @Validated and @Valid are added to the method parameters, the parameters will not be nested. In other words, if there is a propagated list with a Prop pid that is empty or negative, the entry parameter verification will not be detected.
In order to be able to perform nested verification, it is necessary to manually specify on the props field of the Item entity that the entities in this field must also be verified. Since @Validated cannot be used on member attributes (fields), but @Valid can be added on member attributes (fields), and the @Valid class annotation also shows that it supports nested verification function, we can infer that: @Valid cannot be automatically nested verification when added to method parameters, but is used on the corresponding fields of the class that need to be nested verification, and is used to cooperate with @Validated or @Valid on method parameters for nested verification.
We modify the Item class as follows:
public class Item { @NotNull(message = "id cannot be empty") @Min(value = 1, message = "id must be a positive integer") private Long id; @Valid // Nested verification must be @Valid @NotNull(message = "props cannot be empty") @Size(min = 1, message = "Props must have at least one custom property") private List<Prop> props; }
Then we can use @Validated or @Valid on the addItem function of the ItemController to perform nested verification of the entry parameters of the Item. If the corresponding field of the props in the Item is empty, the Spring Validation framework will detect it and the bindingResult will record the corresponding error.
A brief analysis of the principle of Spring Validation
Now let’s briefly analyze the principle of Spring verification function.
Method-level parameter verification implementation principle
The so-called method-level verification refers to directly adding constraints such as @NotNull and @NotEmpty to the method parameters.
for example
@GetMapping("/getUser") @ResponseBody public R getUser(@NotNull(message = "userId cannot be empty") Integer userId){ // }
or
@Validated @Service public class ValidatorService { private static final Logger logger = (); public String show(@NotNull(message = "Can't be empty") @Min(value = 18, message = "Minimum 18") String age) { ("age = {}", age); return age; } }
All are method-level verification. This method can be used on any Spring Bean method, such as Controller/Service, etc.
The underlying implementation principle is AOP. Specifically, it is to dynamically register the AOP section through the MethodValidationPostProcessor, and then use MethodValidationInterceptor to weave and enhance the point-cutting method.
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean { @Override public void afterPropertiesSet() { //Create facets for all beans marked with `@Validated` Pointcut pointcut = new AnnotationMatchingPointcut(, true); //Create Advisor for enhancement = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice()); } //Creating Advice is essentially a method interceptor protected Advice createMethodValidationAdvice(@Nullable Validator validator) { return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); } }
Then take a look at MethodValidationInterceptor:
public class MethodValidationInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { //Skip directly without enhancing methods if (isFactoryBeanMetadataMethod(())) { return (); } //Get grouping information Class[] groups = determineValidationGroups(invocation); ExecutableValidator execVal = (); Method methodToValidate = (); Set<constraintviolation> result; try { //The method is used to verify the parameters, and it is ultimately entrusted to the Hibernate Validator for verification. result = ( (), methodToValidate, (), groups); } catch (IllegalArgumentException ex) { ... } // Throw it directly if there is an exception if (!()) { throw new ConstraintViolationException(result); } //Real method call Object returnValue = (); //Check the return value, and ultimately, it is entrusted to Hibernate Validator for verification result = ((), methodToValidate, returnValue, groups); // Throw it directly if there is an exception if (!()) { throw new ConstraintViolationException(result); } return returnValue; } }
DTO level verification
@PostMapping("/saveUser") @ResponseBody //Note: If the parameter in the method is an object type, you must add @Validated in front of the parameter object.public R saveUser(@Validated @RequestBody UserDTO userDTO){ (100); return (userDTO); }
This kind of verification belongs to the DTO level. In spring-mvc, RequestResponseBodyMethodProcessor is used to parse the parameters of the @RequestBody annotation and to process the return value of the @ResponseBody annotation method. Obviously, the logic for performing parameter verification is definitely in resolveArgument(), the method of parsing parameters.
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { parameter = (); //Embroidery the requested data into the DTO object Object arg = readWithMessageConverters(webRequest, parameter, ()); String name = (parameter); if (binderFactory != null) { WebDataBinder binder = (webRequest, arg, name); if (arg != null) { // Perform data verification validateIfApplicable(binder, parameter); if (().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, ()); } } if (mavContainer != null) { (BindingResult.MODEL_KEY_PREFIX + name, ()); } } return adaptArgumentIfNecessary(arg, parameter); } }
As you can see, resolveArgument() calls validateIfApplicable() for parameter verification.
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { // Get parameter annotations, such as @RequestBody, @Valid, @Validated Annotation[] annotations = (); for (Annotation ann : annotations) { // Try to get the @Validated annotation first Validated validatedAnn = (ann, ); //If @Validated is marked directly, then check directly. //If not, then determine whether there is annotation starting with Valid before the parameter. if (validatedAnn != null || ().getSimpleName().startsWith("Valid")) { Object hints = (validatedAnn != null ? () : (ann)); Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); //Perform verification (validationHints); break; } } }
After seeing this, you should understand why the two annotations @Validated and @Valid can be mixed in this scenario. Let's continue to look at the implementation of () next.
Finally, it was found that the underlying layer finally called Hibernate Validator for real verification.
Unified handling of 404 and other errors
refer toblog
refer to
Spring Validation implementation principle and how to apply it
SpringBoot parameter checksum international use
The difference between @Valid and @ValidatedS
Pring Validation best practices and implementation principles, parameter verification is not that simple!
This is the end of this article about how to use SpringBoot for elegant data verification. For more related SpringBoot data verification content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!