Does SpringBoot also use if to verify parameters? That’s what old drivers do!



Hello, I'm a passerby. For more high-quality articles, see my personal blog: http://itsoku.com

In project development, we often encounter various parameter verification, especially the verification of form parameters. When there are not many parameters, we can manually verify them in the controller, but once we encounter a post interface with many parameters that need to be verified, it will be exhausting to verify one by one.

In fact, the Spring framework provides us with an API for object verification, which can help us save the trouble of manually verifying interface parameters one by one.

This article will systematically study the use of Spring Validation with you, and understand the principles.

Easy to use

The Java API specification (JSR303) defines the standard validation-api for Bean validation, but does not provide an implementation. hibernate validation is the implementation of this specification, and adds validation annotations such as @Email, @Length, etc.

Spring Validation is a secondary encapsulation of hibernate validation, which is used to support spring mvc parameter automatic verification. Next, we take the spring-boot project as an example to introduce the use of Spring Validation.

Introducing dependencies

If the spring-boot version is less than 2.3.x, spring-boot-starter-web will automatically import 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 the business, parameter verification must be done at the Controller layer! In most cases, request parameters are divided into the following two forms:

  • POST, PUT requests, use requestBody to pass parameters;

  • GET request, use requestParam/PathVariable to pass parameters.

In fact, whether it is requestBody parameter verification or method-level verification, it is ultimately to call Hibernate Validator to perform verification, and Spring Validation is just a layer of encapsulation. Let’s briefly introduce the parameter verification of requestBody and requestParam/PathVariable below!

requestBody parameter verification

POST, PUT requests generally use requestBody to pass parameters, in this case, the backend uses DTO objects take over. Just add the @Validated annotation to the DTO object to realize automatic parameter validation. For example, there is an interface for saving User, which requires the length of userName to be 2-10, and the length of account and password It is 6-20. If the verification fails, a MethodArgumentNotValidException exception will be thrown, and Spring will convert it to a 400 (Bad Request) request by default.

DTO means Data Transfer Object (Data Transfer Object), which is used for interactive transmission between server and client. The Bean object used to receive request parameters can be represented in the spring-web project.

Declaring 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;
}

Declaring validation annotations on method parameters

@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
    // Business logic processing will only be executed if the verification is passed
    return Result.ok();
}

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

requestParam/PathVariable parameter verification

GET requests generally use requestParam/PathVariable to pass parameters. If there are many parameters (such as more than 6), it is recommended to use the DTO object to receive.

Otherwise, it is recommended to flatten the parameters one by one into the method input parameters. In this case, you must mark the @Validated annotation on the Controller class, and declare constraint annotations (such as @Min, etc.) . If the validation 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 only be executed if 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 only be executed if the verification is passed
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(100000000000000003L);
        userDTO.setAccount(account);
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        return Result.ok(userDTO);
    }
}

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 status code of http must return 200, and the business code is used to distinguish the abnormal situation of the system.

@RestControllerAdvice
public class CommonExceptionHandler {

    @ExceptionHandler({MethodArgumentNotValidException. class})
    @ResponseStatus(HttpStatus. OK)
    @ResponseBody
    public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex. getBindingResult();
        StringBuilder sb = new StringBuilder("Validation 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 usage

Group verification

In an actual project, multiple methods may need to use the same DTO class to receive parameters, and the validation rules of different methods may be different. At this time, simply adding constraint annotations to the fields of the DTO class cannot solve this problem. Therefore, spring-validation supports the function of group verification, which is specially used to solve such problems.

Still the above example, such as When saving User, UserId can be empty, but when updating User, UserId The value of code> must be **>=10000000000000000L**; the verification rules of other fields are the same in both cases. The code example of using group verification at this time is as follows:

The applicable grouping 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 group when updating
     */
    public interface Update {
    }
}

Specify the verification group on the @Validated annotation

@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {
    // Business logic processing will only be executed if the verification is passed
    return Result.ok();
}

@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {
    // Business logic processing will only be executed if the verification is passed
    return Result.ok();
}

Nested validation

In the previous example, the fields in the DTO class are all basic data types and String types. But in actual scenarios, it is possible that a certain field is also an object. In this case, nested validation can be used first.

For example, when User information is saved above, it also contains Job information. It should be noted that at this time, the corresponding field of the DTO class must be marked with the @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 group when updating
     */
    public interface Update {
    }
}

Nested validations can be used in conjunction with grouped validations. In addition, the nested collection verification will verify each item in the collection, for example, the List field will verify each list in this list code>Job objects are verified

Collection verification

If the request body directly passes the json array to the background, and you want to perform parameter verification on each item in the array. At this point, if we directly use list or set under java.util.Collection to receive data, the parameter verification will not take effect! We can use a custom list collection 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 the @Valid annotation
    public List<E> list = new ArrayList<>();

    // Be sure to remember to override the toString method
    @Override
    public String toString() {
        return list.toString();
    }
}

The @Delegate annotation is limited by the lombok version, and 1.18.6 and later versions can support it. If the verification fails, a NotReadablePropertyException will be thrown, which can also be handled with a unified exception.

For example, if 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 only be executed if the verification is passed
    return Result.ok();
}

Custom validation

Business requirements are always more complex than these simple checks provided by the framework, and we can customize checks to meet our needs.

Customizing spring validation is very simple. Suppose we customize the encrypted id (composed of numbers or a-f letters, 32-256 length) verification, 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 "encrypted id format error";

    // group
    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 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!

Programmatic validation

The above examples are all based on annotations to achieve 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;

// programmatic validation
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
    Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class);
    // If the verification is passed, validate is empty; otherwise, validate contains items that have not passed the verification
    if (validate. isEmpty()) {
        // Business logic processing will only be executed if the verification is passed

    } else {
        for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) {
            // Validation failed, do other logic
            System.out.println(userDTOConstraintViolation);
        }
    }
    return Result.ok();
}

Fail Fast

Spring Validation will verify all fields by default before throwing an exception. You can enable the Fali Fast mode through some simple configurations, and return immediately once the verification fails.

@Bean
public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            // fail-fast mode
            .failFast(true)
            .buildValidatorFactory();
    return validatorFactory. getValidator();
}

Validator implementation principle

Principle of requestBody parameter verification

In spring-mvc, RequestResponseBodyMethodProcessor is used to parse the parameters of @RequestBody and process the @ResponseBody annotation method of the return value. Obviously, the logic for performing parameter verification must be in the method resolveArgument() 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 the 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 validation
                validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() & amp; & amp; 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);
    }
}

It can be seen that 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 directly marked, then the validation is enabled directly.
        //If not, then judge whether there is a comment starting with Valid 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 validation
            binder.validate(validationHints);
            break;
        }
    }
}

It can be seen that 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 directly marked, then the validation is enabled directly.
        //If not, then judge whether there is a comment starting with Valid 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 validation
            binder.validate(validationHints);
            break;
        }
    }
}

Seeing this, you should be able to understand why the two annotations @Validated and @Valid can be mixed in this scenario. Let’s continue to look at the implementation of WebDataBinder.validate().

@Override
public void validate(Object target, Errors errors, Object... validationHints) {
    if (this. targetValidator != null) {
        processConstraintViolations(
            //Call Hibernate Validator here to perform real validation
            this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
    }
}

Finally, it was found that the bottom layer finally called Hibernate Validator for real validation processing.

Principle of parameter verification at the method level

The above-mentioned method of flattening parameters into method parameters one by one, and then declaring constraint annotations in front of each parameter is method-level parameter verification.

In fact, this method can be used in any Spring Bean method, such as Controller/Service and so on. The underlying implementation principle is AOP. Specifically, AOP aspects are dynamically registered through MethodValidationPostProcessor, and then MethodValidationInterceptor is used to weave and enhance point-cutting methods.

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {
    @Override
    public void afterPropertiesSet() {
        //Create aspects for all `@Validated` marked beans
        Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
        //Create Advisor for enhancement
        this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
    }

    //Create Advice, which is essentially a method interceptor
    protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
        return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
    }
}

Then look at MethodValidationInterceptor:

public class MethodValidationInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // No need to enhance the method, skip directly
        if (isFactoryBeanMetadataMethod(invocation. getMethod())) {
            return invocation. proceed();
        }
        //Get group information
        Class<?>[] groups = determineValidationGroups(invocation);
        ExecutableValidator execVal = this.validator.forExecutables();
        Method methodToValidate = invocation. getMethod();
        Set<ConstraintViolation<Object>> result;
        try {
            //The method enters the parameter verification, and finally entrusts it to Hibernate Validator to verify
            result = execVal.validateParameters(
                invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        catch (IllegalArgumentException ex) {
            ...
        }
        //An exception is thrown directly
        if (!result. isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        //actual method call
        Object returnValue = invocation. proceed();
        //Verify the return value, and finally entrust it to Hibernate Validator to verify
        result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
        //An exception is thrown directly
        if (!result. isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        return returnValue;
    }
}

In fact, whether it is requestBody parameter verification or method-level verification, it is ultimately to call Hibernate Validator to perform verification, and Spring Validation is just Made a layer of encapsulation.

Source: https://juejin.cn/post/7080419992386142215

191282d816d3593fd765873bdd935b57.gif




↓↓↓ Click to read the original text and go directly to my personal blog. Are you watching?