This article will introduce the best practices and implementation principles of Spring Validation in various scenarios in detail!
Project source code: spring-validation
1, Simple use
The Java API specification (JSR303) defines the standard validation API for Bean verification, but does not provide an implementation. hibernate validation is the implementation of this specification and adds verification annotations, such as @ Email, @ Length, etc.
Spring Validation is a secondary encapsulation of hibernate validation, which is used to support automatic verification of spring mvc parameters. Next, we take the spring boot project as an example to introduce the use of Spring Validation.
1. Introduce dependency
If the spring boot version is less than 2.3.x, the spring boot starter web will automatically pass in the hibernate validator dependency. If the spring boot version is greater than 2.3.x, you need to manually introduce dependencies:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.1.Final</version> </dependency>
For web services, in order to prevent illegal parameters from affecting business, parameter verification must be done in the Controller layer! In most cases, the request parameters are divided into the following two forms:
- For POST and PUT requests, use requestBody to pass parameters;
- For GET requests, use requestParam/PathVariable to pass parameters.
Let's briefly introduce the parameter verification practice of requestBody and requestParam/PathVariable!
2. requestBody parameter verification
POST and PUT requests generally use requestBody to pass parameters. In this case, the back end uses DTO object to receive.
As long as the @ Validated annotation is added to the DTO object, automatic parameter verification can be realized.
For example, there is an interface for saving users. The length of userName is 2-10 and the age is between 1 and 99. If the verification fails, a MethodArgumentNotValidException exception will be thrown, and Spring will turn it into a 400 (Bad Request) request by default.
- Declare constraint annotations on DTO fields
@Data public class UserDTO { @NotNull private Long userId; @Length(min = 2, max = 10) private String userName; @Min(1) @Max(99) private int age;
- Declare a validation annotation on a method parameter
@PostMapping("/save") public Result saveUser(@RequestBody @Validated UserQO userDTO) { // Business logic processing will not be executed until the verification is passed return Result.success(); }
In this case, @ Valid and @ Validated can be used.
3. requestParam parameter verification
GET requests generally use requestParam/PathVariable to pass parameters. If there are many parameters (for example, more than 6), it is recommended to use DTO object to receive. Otherwise, it is recommended to tile parameters into method parameters.
In this case, the @ Validated annotation must be marked on the Controller class, and the constraint annotation (such as @ Min, etc.) must be declared on the input parameter. If the verification fails, a ConstraintViolationException is thrown.
@RequestMapping("/api/user") @RestController @Validated public class UserController { // Path variable @GetMapping("{userId}") public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) { // Business logic processing will not be executed until the verification is passed return Result.success(); } // Query parameters @GetMapping("getByUserName") public Result getByUserName(@Length(min = 6, max = 20) @NotNull String userName) { // Business logic processing will not be executed until the verification is passed return Result.success(); } }
4. Unified exception handling
As mentioned earlier, if the verification fails, a MethodArgumentNotValidException or ConstraintViolationException will be thrown. In actual project development, unified exception handling is usually used to return a more friendly prompt. For example, our system requires that no matter what exception is sent, the http status code must return 200, and the business code can distinguish the system exceptions.
@RestControllerAdvice @Slf4j public class CommonExceptionHandler { @ExceptionHandler({MethodArgumentNotValidException.class}) @ResponseStatus(HttpStatus.OK) @ResponseBody public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { BindingResult bindingResult = ex.getBindingResult(); StringJoiner joiner = new StringJoiner(","); for (FieldError fieldError : bindingResult.getFieldErrors()) { joiner.add(fieldError.getField()).add(":").add(fieldError.getDefaultMessage()); } String msg = joiner.toString(); return Result.fail(BusinessCode.MISSING_PARAMETERS, msg); } @ExceptionHandler({ConstraintViolationException.class}) @ResponseStatus(HttpStatus.OK) @ResponseBody public Result handleConstraintViolationException(ConstraintViolationException ex) { return Result.fail(BusinessCode.MISSING_PARAMETERS, ex.getMessage()); } @ExceptionHandler({NotReadablePropertyException.class}) @ResponseStatus(HttpStatus.OK) @ResponseBody public Result handleNotReadablePropertyException(NotReadablePropertyException ex) { return Result.fail(BusinessCode.MISSING_PARAMETERS, ex.getMessage()); } @ExceptionHandler({Exception.class}) @ResponseStatus(HttpStatus.OK) @ResponseBody public Result handleException(Exception ex) { log.error("Unknown system error", ex); return Result.fail(BusinessCode.MISSING_PARAMETERS, ex.getMessage()); } }
2, Advanced use
1. Group check
In an actual project, multiple methods may need to use the same DTO class to receive parameters, and the verification rules of different methods are likely to be different. At this time, simply adding constraint annotations to the fields of DTO classes cannot solve this problem. Therefore, spring validation supports the function of group verification, which is specially used to solve such problems. In the above example, for example, when saving the User, the userid can be empty, but when updating the User, the userid value cannot be empty; The verification rules for other fields are the same in both cases. The code example of using group verification at this time is as follows:
@Data public class UserDTO { @NotNull(groups = Update.class) @Null(groups = Save.class) private Long userId; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String userName; @Min(value = 1, groups = {Save.class, Update.class}) @Max(value = 99, groups = {Save.class, Update.class}) private int age; /** * Verify grouping when saving */ public interface Save { } /** * Verify grouping when updating */ public interface Update { } }
- @The validation group is specified on the Validated annotation
@PostMapping("/save") public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) { // Business logic processing will not be executed until the verification is passed return Result.success(); } @PostMapping("/update") public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) { // Business logic processing will not be executed until the verification is passed return Result.success(); }
Optimization. Normally, if a DTO is used for general purpose, it is generally similar to userId. Saving and updating are different, but others will be the same. Therefore, it is not necessary to add groups = {Save.class, Update.class}) to each attribute. We can do this
@Data public class UserGroupsDTO { @NotNull(groups = Update.class) @Null(groups = Save.class) private Long userId; @NotNull @Length(min = 2, max = 10) private String userName; @Min(1) @Max(99) private int age; }
We only need userId grouping, and others do not need grouping.
The interface request is also slightly modified
@PostMapping("/save") public Result saveUser(@RequestBody @Validated({UserDTO.Save.class, Default.class}) UserDTO userDTO) { // Business logic processing will not be executed until the verification is passed return Result.success(); } @PostMapping("/update") public Result updateUser(@RequestBody @Validated({UserDTO.Update.class, Default.class}) UserDTO userDTO) { // Business logic processing will not be executed until the verification is passed return Result.success(); }
Just pass in one more default group Default.class here. Is it easier than the above.
2. Nested check
In the previous example, the fields in the DTO class are basic data types and String types. However, in the actual scenario, a field may also be an object. In this case, you can use nested verification first. For example, the User information is saved with the Interest information. It should be noted that,
At this time, the corresponding field of DTO class must be marked with @ Valid annotation.
@Data public class UserGroupsDTO { @NotNull(groups = Update.class) @Null(groups = Save.class) private Long userId; @NotNull @Length(min = 2, max = 10) private String userName; @Min(1) @Max(99) private int age; @NotNull @Valid private Interest interest; /** * Interest entity */ @Data public static class Interest { @Min(1) private Long interestId; @NotNull @Length(min = 2, max = 10) private String interestName; } /** * Verify grouping when saving */ public interface Save { } /** * Verify grouping when updating */ public interface Update { } }
Nested checks can be used in conjunction with group checks. In addition, nested set verification will verify every item in the set. For example, the list < job > field will verify every Interest object in the list.
3. Set verification
If the request body directly passes the json array to the background, and you want to verify the parameters of each item in the array. At this time, if we directly use the list or set under java.util.Collection to receive data, the parameter verification will not take effect! We can use the custom list collection to receive parameters:
@Data public class ValidationList<E> implements List<E> { @Delegate // @Delegate is a lombok annotation @Valid // Be sure to add @ Valid annotation public List<E> list = new ArrayList<>(); /** * Remember to override the toString method */ @Override public String toString() { return list.toString(); } }
@The Delegate annotation is limited by the lombok version. Versions above 1.18.6 can support it. Otherwise, an error will be reported when starting. If the verification fails, a NotReadablePropertyException will be thrown, which can also be handled with a unified exception.
For example, we need to save multiple User objects at one time. The method of the Controller layer can be written as follows:
@PostMapping("/saveList") public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) { // Business logic processing will not be executed until the verification is passed return Result.success(); }
4. Custom verification
Business requirements are always more complex than these simple checks provided by the framework. We can customize the checks to meet our needs. Customizing spring validation is very simple. Suppose we customize sex (only man and woman) validation, which is mainly divided into two steps:
- Custom constraint annotation
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {SexValidator.class}) public @interface Sex { // Default error message String message() default "Gender format error"; // grouping Class<?>[] groups() default {}; // load Class<? extends Payload>[] payload() default {}; }
- Implement the ConstraintValidator interface to write a constraint validator
public class SexValidator implements ConstraintValidator<Sex, String> { private static final String MAN = "man"; private static final String WOMAN = "woman"; @Override public boolean isValid(String value, ConstraintValidatorContext context) { // Check only if it is not null if (value != null) { if(!Objects.equals(value,MAN) && !Objects.equals(value,WOMAN)) { return Boolean.FALSE; } } return Boolean.TRUE; } }
In this way, we can use @ Sex for parameter verification!
5. Programming verification
The above examples are based on annotations to implement automatic verification. In some cases, we may want to call verification programmatically. At this time, you can inject the javax.validation.Validator object, and then call its api.
@Autowired private javax.validation.Validator globalValidator; // Programming verification @PostMapping("/saveWithCodingValidate") @ApiOperation("Programming verification") public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) { Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class); // If the verification passes, validate is null; Otherwise, validate contains items that fail the verification if (validate.isEmpty()) { // Business logic processing will not be executed until the verification is passed } else { for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) { // Verification failed, do other logic System.out.println(userDTOConstraintViolation); } } return Result.success(); }
6. Fail fast
By default, Spring Validation will verify all fields before throwing an exception. You can enable the Fali Fast mode through some simple configurations. Once the verification fails, it will return immediately.
@Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() // Fast failure mode .failFast(true) .buildValidatorFactory(); return validatorFactory.getValidator(); }
7. Difference between @ Valid and @ Validated
difference | @Valid | @Validated |
---|---|---|
Provider | JSR-303 specification | Spring |
Whether grouping is supported | I won't support it | support |
marking position | METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE | TYPE, METHOD, PARAMETER |
Nested check | support | I won't support it |
3, Implementation principle
1. Implementation principle of requestBody parameter verification
In spring MVC, the RequestResponseBodyMethodProcessor is used to parse the parameters of the @ RequestBody annotation and process the return value of the @ ResponseBody annotation method. Obviously, the logic for performing parameter verification must be in the resolveArgument() method for parsing parameters:
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { parameter = parameter.nestedIfOptional(); //Encapsulate the request data into a DTO object Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); String name = Conventions.getVariableNameForParameter(parameter); if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); if (arg != null) { // Perform data verification validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } if (mavContainer != null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } } 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 = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { // Try to get the @ Validated annotation first Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); //If @ Validated is marked directly, the verification can be started directly. //If not, judge whether there is a Valid start annotation before the parameter. if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); //Perform verification binder.validate(validationHints); break; } } }
After seeing this, you should understand why * * @ Validated and @ Valid * * annotations can be mixed in this scenario. Let's continue to look at the WebDataBinder.validate() implementation.
@Override public void validate(Object target, Errors errors, Object... validationHints) { if (this.targetValidator != null) { processConstraintViolations( //Hibernate Validator is called here to perform real verification this.targetValidator.validate(target, asValidationGroups(validationHints)), errors); } }
Finally, it is found that the underlying layer finally calls Hibernate Validator for real verification processing.
2. Implementation principle of method level parameter verification
As mentioned above, tiling parameters into method parameters one by one, and then declaring the verification method of constraint annotation in front of each parameter is method level parameter verification. In fact, this method can be used for any Spring Bean method, such as Controller/Service. Its underlying implementation principle is AOP. Specifically, it dynamically registers AOP facets through the MethodValidationPostProcessor, and then uses the MethodValidationInterceptor to weave enhancements to the pointcut method.
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean { @Override public void afterPropertiesSet() { //Create facets for all ` @ Validated 'annotated beans Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); //Create Advisor for enhancement this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator)); } //Creating Advice is essentially a method interceptor protected Advice createMethodValidationAdvice(@Nullable Validator validator) { return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); } }
Next, take a look at the MethodValidationInterceptor:
public class MethodValidationInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { //Skip the method without enhancement if (isFactoryBeanMetadataMethod(invocation.getMethod())) { return invocation.proceed(); } //Get grouping information Class<?>[] groups = determineValidationGroups(invocation); ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation.getMethod(); Set<ConstraintViolation<Object>> result; try { //Method input parameter verification is finally delegated to Hibernate Validator for verification result = execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { ... } //Throw an exception directly if (!result.isEmpty()) { throw new ConstraintViolationException(result); } //Real method calls Object returnValue = invocation.proceed(); //Verify the returned value, and finally delegate it to Hibernate Validator for verification result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups); //Throw an exception directly if (!result.isEmpty()) { throw new ConstraintViolationException(result); } return returnValue; } }
In fact, whether it is requestBody parameter verification or method level verification, Hibernate Validator is called to perform verification. Spring Validation is just a layer of encapsulation.
thank
This article is basically from https://juejin.cn/post/6856541106626363399
I just made a demo and modified some of the mistakes.