Spring Boot implements various parameter verification. It is well written. It is recommended to collect it!

Keywords: Java

Author: Wu Luqi\
Source: https://juejin.cn/post/685654...

I also wrote an article about the use of Spring Validation before, but I still feel superficial. This time I intend to thoroughly understand Spring Validation.

This article will introduce the best practices and implementation principles of Spring Validation in various scenarios in detail!

Project source code: Spring validation( https://github.com/chentianmi...)

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 the automatic verification of spring mvc parameters. Next, we take the spring boot project as an example to introduce the use of Spring Validation.

Introduce dependency

If the spring boot version is less than 2.3.x, 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 import the dependency:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.1.Final</version>
</dependency>

Recommend a basic Spring Boot tutorial and practical example:
https://github.com/javastacks...

For web services, in order to prevent illegal parameters from affecting business, parameter verification must be done at the Controller layer! In most cases, request parameters are divided into the following two forms:

  1. For POST and PUT requests, use requestBody to pass parameters;
  2. For GET requests, use requestParam/PathVariable to pass parameters.

Let's briefly introduce the parameter verification practice of requestBody and requestParam/PathVariable!

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 required to be 2-10, and the length of account and password fields is 6-20. If the verification fails, a MethodArgumentNotValidException exception will be thrown, and Spring will turn it into a 400 (Bad Request) request by default.

DTO represents Data Transfer Object, which is used for interactive transmission between server and client. In spring web project, it can represent Bean object used to receive request parameters.

  • Declare constraint annotations on DTO fields
@Data
public class UserDTO {

    private Long userId;

    @NotNull
    @Length(min = 2, max = 10)
    private String userName;

    @NotNull
    @Length(min = 6, max = 20)
    private String account;

    @NotNull
    @Length(min = 6, max = 20)
    private String password;
}
  • Declare a validation annotation on a method parameter
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
    // Business logic processing will not be executed until the verification is passed
    return Result.ok();
}

In this case, @ Valid and @ Validated can be used.

requestParam/PathVariable parameter verification

Generally, requestParam/PathVariable is used to pass parameters in GET requests. 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 will be thrown. The code example is as follows:

@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
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(userId);
        userDTO.setAccount("11111111111111111");
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        return Result.ok(userDTO);
    }

    // Query parameters
    @GetMapping("getByAccount")
    public Result getByAccount(@Length(min = 6, max = 20) @NotNull String  account) {
        // Business logic processing will not be executed until the verification is passed
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(10000000000000003L);
        userDTO.setAccount(account);
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        return Result.ok(userDTO);
    }
}

Unified exception handling

As mentioned earlier, if the verification fails, 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
public class CommonExceptionHandler {

    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("Verification failed:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(": ").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
       return Result.fail(BusinessCode.Parameter verification failed, msg);
    }

    @ExceptionHandler({ConstraintViolationException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleConstraintViolationException(ConstraintViolationException ex) {
        return Result.fail(BusinessCode.Parameter verification failed, ex.getMessage());
    }
}

Advanced use

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 may 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 specifically used to solve such problems. For example, when saving the User, the UserId can be empty, but when updating the User, the value of the UserId must be > = 1000000000000 L; the verification rules of other fields are the same in the two cases. The code examples of using group verification at this time are as follows:

  • The applicable group information groups are declared on the constraint annotation
@Data
public class UserDTO {

    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    /**
     * 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.ok();
}

@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.ok();
}

Nested check

In the previous example, the fields in the DTO class are all 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 Job information. It should be noted that the corresponding field of DTO class must be marked with @ Valid annotation.

@Data
public class UserDTO {

    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    @NotNull(groups = {Save.class, Update.class})
    @Valid
    private Job job;

    @Data
    public static class Job {

        @Min(value = 1, groups = Update.class)
        private Long jobId;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String jobName;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String position;
    }

    /**
     * Verify grouping when saving
     */
    public interface Save {
    }

    /**
     * Verify grouping when updating
     */
    public interface Update {
    }
}

Nested verification can be combined with group verification. In addition, nested set verification will verify every item in the set. For example, the list < Job > field will verify every Job object in the list.

Set verification

If the request body directly passes the json array to the background and wants 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 self-defined list set to receive parameters:

  • Wrap the List type and declare the @ Valid annotation
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, and can be supported in versions above 1.18.6. 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.ok();
}

Recommend a basic Spring Boot tutorial and practical example:
https://github.com/javastacks...

Custom verification

Business requirements are always much more complex than these simple verifications provided by the framework. We can customize the verifications to meet our requirements. Customizing spring validation is very simple. Suppose we customize the verification of encryption id (composed of numbers or a-f letters, 32-256 in length), which is mainly divided into two steps:

  • Custom constraint annotation
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface EncryptId {

    // Default error message
    String message() default "encryption id Format error";

    // grouping
    Class<?>[] groups() default {};

    // load
    Class<? extends Payload>[] payload() default {};
}
  • Implement the ConstraintValidator interface to write a constraint validator
public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {

    private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // Check only if it is not null
        if (value != null) {
            Matcher matcher = PATTERN.matcher(value);
            return matcher.find();
        }
        return true;
    }
}

In this way, we can use @ EncryptId for parameter verification!

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")
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.ok();
}

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();
}

@Difference between Valid and @ Validated

difference@Valid@Validated
ProviderJSR-303 specificationSpring
Whether grouping is supportedI won't support itsupport
marking position METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USETYPE, METHOD, PARAMETER
Nested checksupportI won't support it

Implementation principle

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.

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.

Recent hot article recommendations:

1.1000 + Java interview questions and answers (2021 latest version)

2.Stop playing if/ else on the full screen. Try the strategy mode. It's really fragrant!!

3.what the fuck! What is the new syntax of xx ≠ null in Java?

4.Spring Boot 2.5 heavy release, dark mode is too explosive!

5.Java development manual (Songshan version) is the latest release. Download it quickly!

Feel good, don't forget to like + forward!

Posted by maddogandnoriko on Fri, 19 Nov 2021 01:42:26 -0800