I have written an article about the use of Spring Validation before, but I still feel that I am floating on the surface. This time I intend to understand Spring Validation thoroughly. This article details best practices and implementation principles for Spring Validation in various scenarios. Project source code: Spring-Validation

Simple to use

The Java API specification (JSR303) defines a standard validation-API for Bean validation, but does not provide an implementation. Hibernate Validation is an implementation of this specification and adds validation annotations such as @email, @length, and so on. Spring Validation is a secondary encapsulation of Hibernate Validation to support automatic Validation of Spring MVC parameters. Let’s use spring Validation as an example for the Spring-boot project.

Introduction of depend on

If the spring-boot version is less than 2.3.x, the Spring-boot-starter -web automatically passes in hibernate-Validator dependencies. If the spring-boot version is greater than 2.3.x, manually import the dependencies:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>The 6.0.1. The Final</version>
</dependency>
Copy the code

For Web services, in order to prevent the impact of illegal parameters on the business, it is necessary to do parameter verification in the Controller layer! In most cases, request parameters come in one of two forms:

  1. POST,PUTRequest, userequestBodyPass parameter;
  2. GETRequest, userequestParam/PathVariablePass parameters.

Here we introduces the requestBody and requestParam/PathVariable parameter calibration of actual combat!

requestBodyParameter calibration

POST and PUT requests typically use requestBody to pass parameters, in which case the back end uses a DTO object to receive. Automatic parameter validation can be achieved simply by adding the @Validated annotation to the DTO object. For example, an interface that holds users requires userName to be 2-10 in length and the Account and password fields to be 6-20 in length. If validation fails, throws MethodArgumentNotValidException abnormalities, default will Spring into 400 (Bad Request) Request.

DTO represents a Data Transfer Object, which is used for interactive transmission between a server and a client. In a Spring-Web project, you can represent Bean objects that are used to receive request parameters.

  • inDTODeclare a constraint annotation on a field
@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;
}

Copy the code
  • Declare validation annotations on method parameters
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
    // The service logic processing is performed only after the verification succeeds
    return Result.ok();
}
Copy the code

In this case, you can use either @Valid or @Validated.

requestParam/PathVariableParameter calibration

GET requests are generally use requestParam/PathVariable refs. If the number of parameters is large (for example, more than 6), it is recommended to use DTO object reception. Otherwise, it is recommended to tile each parameter into the method input. In this case, you must annotate the @validated annotation on the Controller class and declare the constraint annotation (such as @min, etc.) on the input parameters. If the check fails, throws ConstraintViolationException anomalies. The following is a code example:

@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
    // Path variables
    @GetMapping("{userId}")
    public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
        // The service logic processing is performed only after the verification succeeds
        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) {
        // The service logic processing is performed only after the verification succeeds
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(10000000000000003L);
        userDTO.setAccount(account);
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        returnResult.ok(userDTO); }}Copy the code

Unified exception handling

Said before, if the check fails, throws MethodArgumentNotValidException or ConstraintViolationException anomalies. In a real project, uniform exception handling is often used to return a friendlier hint. For example, our system requires that whatever exception is sent, the HTTP status code must return 200, and the business code distinguishes the exception 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("Verification failed :");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(",");
        }
        String msg = sb.toString();
       returnResult.fail(BusinessCode. Parameter verification failed, MSG); }@ExceptionHandler({ConstraintViolationException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleConstraintViolationException(ConstraintViolationException ex) {
        returnResult. Fail (BusinessCode. Parameter verification failed, ex.getMessage()); }}Copy the code

Use the advanced

Packet check

In a real project, multiple methods might need to use the same DTO class to receive parameters, and the validation rules for different methods might be different. At this point, simply adding constraint annotations to the FIELDS of the DTO class will not solve the problem. Therefore, Spring-Validation supports packet validation and is specifically designed to solve this type of problem. For example, when saving User, UserId is null, but when updating User, UserId must be >=10000000000000000L. The verification rules for other fields are the same in the two cases. The following is an example of code for using group verification:

  • The grouping information that applies to the declaration on the constraint annotationgroups
@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;

    /** * Check the group when saving */
    public interface Save {}/** * Check the group */ when updating
    public interface Update {}}Copy the code
  • @ValidatedSpecify a checksum group on the annotation
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {
    // The service logic processing is performed only after the verification succeeds
    return Result.ok();
}

@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {
    // The service logic processing is performed only after the verification succeeds
    return Result.ok();
}
Copy the code

Nested check

In the previous example, the fields in the DTO class were of basic data types and String types. However, in the real world, it is possible that a field is also an object. In this case, nested validation can be used. For example, it can store User information along with Job information. Note that 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;
    }

    /** * Check the group when saving */
    public interface Save {}/** * Check the group */ when updating
    public interface Update {}}Copy the code

Nested checks can be used in combination with packet checks. In addition, nested set verification verifies every item in the set. For example, the List

field verifies every Job object in the List.

A collection of check

If the request body passes a JSON array directly to the backend and you want to validate each item in the array. At this point, if we use the list or set in java.util.Collection to receive the data, parameter validation will not take effect! We can use a custom list collection to receive parameters:

  • packagingListType and declare@Validannotations
public class ValidationList<E> implements List<E> {

    @Delegate @delegate is a Lombok annotation
    @Valid // The @valid annotation must be added
    public List<E> list = new ArrayList<>();

    // Always remember to override the toString method
    @Override
    public String toString(a) {
        returnlist.toString(); }}Copy the code

The @delegate annotation is limited by lombok version, 1.18.6 and above are supported. If the check is not passed, throws NotReadablePropertyException, also can use unified exception processing.

For example, if we need to save multiple User objects at once, the Controller layer method could write:

@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) {
    // The service logic processing is performed only after the verification succeeds
    return Result.ok();
}
Copy the code

Custom check

Business requirements are always more complex than the simple validations provided by the framework, and we can customize validations to meet our needs. Custom Spring Validation is very simple. Suppose we define our own encryption ID (32 to 256 characters in length, consisting of digits or a-F letters). There are two steps:

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

    // Default error message
    String message(a) default"Encryption ID format error";

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

    / / load
    Class<? extends Payload>[] payload() default {};
}
Copy the code
  • implementationConstraintValidatorInterface writing constraint validators
public class EncryptIdValidator implements ConstraintValidator<EncryptId.String> {

    private static final Pattern PATTERN = Pattern.compile("^ / a - f \ \ d {32256} $");

    @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; }}Copy the code

This allows us to use @encryptid for parameter validation!

Programmatic check

The above examples are based on annotations for automatic validation, and in some cases we might want to invoke validation programmatically. This time can be injected javax.mail. Validation. The Validator object, then calls the API.

@Autowired
private javax.validation.Validator globalValidator;

// Programmatic check
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
    Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class);
    // If the validation is successful, validate is null; Otherwise, validate contains items that have not been verified
    if (validate.isEmpty()) {
        // The service logic processing is performed only after the verification succeeds

    } else {
        for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) {
            // Check failed, do other logicSystem.out.println(userDTOConstraintViolation); }}return Result.ok();
}
Copy the code

Fail Fast

By default, Spring Validation validates all fields before throwing an exception. You can enable the Fali Fast mode by performing some simple configurations. If the verification fails, Fali Fast returns immediately.

@Bean
public Validator validator(a) {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            // Fast failure mode
            .failFast(true)
            .buildValidatorFactory();
    return validatorFactory.getValidator();
}
Copy the code

@Validand@ValidatedThe difference between

The difference between @Valid @Validated
The provider JSR – 303 specification Spring
Whether groups are supported Does not support support
Label position METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE TYPE, METHOD, PARAMETER
Nested check support Does not support

Realize the principle of

requestBodyImplementation principle of parameter verification

In spring MVC – RequestResponseBodyMethodProcessor is used to resolve parameters and processing @ @ RequestBody annotation ResponseBody tagging methods return values. Obviously, the logic for performing argument validation must be in the resolveArgument() method:

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 newMethodArgumentNotValidException(parameter, binder.getBindingResult()); }}if(mavContainer ! =null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); }}returnadaptArgumentIfNecessary(arg, parameter); }}Copy the code

As you can see, resolveArgument() calls validateIfApplicable() for parameter validation.

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    // Get the parameter annotations, such as @requestBody, @valid, and @validated
    Annotation[] annotations = parameter.getParameterAnnotations();
    for (Annotation ann : annotations) {
        // First try to get the @Validated annotation
        Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
        // If @validated is used, then check is enabled directly.
        // If not, check whether there is a Valid comment 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; }}}Copy the code

This should help you understand why the @Validated and @Valid annotations can be used concurrently in this scenario. Let’s move on to the WebDatabinder.validate () implementation.

@Override
public void validate(Object target, Errors errors, Object... validationHints) {
    if (this.targetValidator ! =null) {
        processConstraintViolations(
            // The Hibernate Validator is called to perform the actual validation
            this.targetValidator.validate(target, asValidationGroups(validationHints)), errors); }}Copy the code

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

Method level parameter verification implementation principle

The aforementioned method of tiling the parameters one by one into the method parameters and declaring the constraint annotation in front of each parameter is method level parameter validation. In fact, this approach can be applied to any Spring Bean method, such as Controller/Service, and so on. The underlying implementation principle is AOP, specifically by MethodValidationPostProcessor dynamically register AOP aspects, and then use the MethodValidationInterceptor woven into the tangent point methods to enhance.

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {
    @Override
    public void afterPropertiesSet(a) {
        // Create facets for all '@Validated' annotation beans
        Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
        // Create a 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) : newMethodValidationInterceptor()); }}Copy the code

Then look at the MethodValidationInterceptor:

public class MethodValidationInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // Do not need to enhance the method, directly skip
        if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
            return invocation.proceed();
        }
        // Get the group informationClass<? >[] groups = determineValidationGroups(invocation); ExecutableValidator execVal =this.validator.forExecutables();
        Method methodToValidate = invocation.getMethod();
        Set<ConstraintViolation<Object>> result;
        try {
            // Delegate the Validator to the Hibernate Validator
            result = execVal.validateParameters(
                invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        catch (IllegalArgumentException ex) {
            ...
        }
        // Throw an exception
        if(! result.isEmpty()) {throw new ConstraintViolationException(result);
        }
        // The actual method call
        Object returnValue = invocation.proceed();
        // Check the returned value and delegate it to the Hibernate Validator
        result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
        // Throw an exception
        if(! result.isEmpty()) {throw new ConstraintViolationException(result);
        }
        returnreturnValue; }}Copy the code

In fact, both requestBody and method Validation are performed by calling the Hibernate Validator, and Spring Validation is just a layer of encapsulation.

It is not easy to be original. If you think you have written a good article, click 👍 to encourage you

Welcome to my open source project: a lightweight HTTP invocation framework for SpringBoot