The SpingBoot project uses @Validated and @Valid parameter verification

1. What is parameter validation?

One of the problems we often encounter in back-end development is input parameter verification. Simply put, it is to verify the parameters of a method to see if it meets our requirements. For example, the entry requirement is an amount, and you have no restrictions on the front end. If the user randomly comes in with a negative number or a letter, then our interface will report an error.

Therefore, usually we need to verify the parameters of the input parameters at the beginning of the method, and return an error if they do not meet the requirements, and do not proceed further. This verification process is parameter verification.

2. Why do we need unified parameter verification?

Now that we understand what parameter verification is, let’s move on to the next question. Now we are going to add parameter verification. If it is a method, it is very easy. But what if all interface methods of a project need to be verified? If you think about it, it’s a big head, there will be a lot of repetitive and cumbersome codes, and the verification codes and business codes are mixed together, and the coupling is too heavy. So how to solve it?

This is our second question today, we need unified parameter verification. Here we can check the aspect parameters through annotations. You only need to add corresponding annotations to the method you want to verify, and when the method is executed, it will first go through our aspect method to verify the input parameters. In this way, through unified parameter verification, the problems of coupling and repetition mentioned above are solved.

Of course, someone has already provided us with this verification annotation, and it is ready-made and easy to use. This is our protagonist Spring Validator framework and Javax Valid today.

3. The difference between @Validated and @Valid

As we mentioned above, there are already existing frameworks for parameter verification, one is the Spring Validator framework and the other is Javax Valid, so what is the difference between the two?

Let’s talk about Javax Valid first. This is provided by the Java core package, and the package name is validation-api. It follows the standard JSR-303 specification, which is actually a verification standard. Because it is provided by Java, we don’t need to introduce it separately when we use it. The most commonly used annotation it provides is @Valid.

Let’s talk about Spring Validator, which is provided by Spring, and the bottom layer is actually a secondary encapsulation of hibernate-validator. hibernate-validator is a variant of the standard JSR-303 specification mentioned above, but most of them are implemented based on the above specification. The most commonly used annotation provided by the Validator framework is the @Validated we mentioned.

There are many differences between the two in use, first look at the place of use:

  • @Validated: Can be used on types, methods and method parameters. But it cannot be used on member properties (fields)
  • @Valid: Can be used on methods, constructors, method parameters and member properties (fields)

In addition, @Validated supports group verification, and @Valid, as a standard JSR-303 specification, does not yet support group verification. But it supports adding to fields, so it supports nested validation. These specific differences will be mentioned in the following articles.

Now that the concepts are clear, let’s move on to the next question, how to use them?

4. How to use annotations such as @Validated and @Valid for parameter validation?

Here we still take the SpringBoot project as an example:

1. First introduce the jar package. As mentioned above, Valid is provided by Javax, so we don’t need to import it. Just need to introduce Spring Validator.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Copy Code

When we click into the spring-boot-starter-validation package, we can see that hibernate-validatro is actually introduced.

Remarks: Note here that versions prior to Spring Boot 2.3 introduced spring-boot-starter-validation by default, and no additional introduction is required. The following is the official website description:

2. Generally speaking, we only need to verify the interface provided to the front end or the interface that provides external services.

3. Parameter verification is usually divided into several situations, which we need to discuss separately:

3.1 Verify RequestParam parameter

@GetMapping(value = "/aaa")
public void selectAaa(@RequestParam("usercode") @NotBlank String code){
    }
Copy Code

This is the simplest. For example, to verify the parameter code of the String type, you only need to add the @NotBlank annotation before the method parameter to verify that the input parameter code is not empty. If it is empty, an error will be reported. But here is one thing to note, When validating the RequestParam parameter, you need to add the @Validated annotation to the class.

3.2 Verify PathVariable parameters

This is the same as verifying the RequestParam parameter. Add the @Validated annotation to the class, enter the specific method that needs to be verified, and add the corresponding @NotBlank, @Min and other annotations for verification.

3.3 Verify form submission parameters

When the front-end form is submitted, the request type contentType is application/x-www-form-urlencoded or multipart/form-data. At this time, the parameters are also brought in the form of key1=value1 & key2=value2. Usually we can use the RequestParam parameter to receive, but when there are many input parameters, we generally receive them as entity classes. Like this:

public class demo{
    public void selectAaa(@Validated UserForm form){
    xxx business code
    }
}


@Data
@Accessors(chain = true)
public class UserForm {
    @NotBlank(message = "code cannot be empty")
    private String code;
    ...
}
Copy Code

Here we add @Validated or @Valid annotations to verify that the entity UserForm form enters the parameters, and the specific verification is in the entity through @NotBlank to verify that the code is not empty.

3.4 Verify RequestBody input parameters

This is also our most commonly used verification method, after all, Post requests still account for the majority.

@PostMapping(value = "/aaa")
public void selectAaa(@RequestBody @Validated UserForm form){
    }
    
@Data
@Accessors(chain = true)
public class UserForm {
@NotBlank(message = "code cannot be empty")
private String code;
...
}

Copy Code

Above we added @Validated or @Valid annotations to the input entity UserForm to indicate validation. For specific verification, we use the @NotBlank annotation on the field code in the UserForm entity to perform non-null verification. In this way, through several annotations, the verification of the input parameter field can be realized. Note here the difference from 3.3. Here we add the @RequestBody annotation to parse the parameters in json format from the request body to the entity. And 3.3 is brought by the url parameter form of key1=value1 & amp;key2=value2, and parsed to the entity from the path.

Why do we need to distinguish between various situations, mainly for our exception handling below, continue to look down.

4. After the verification is added, if the input parameters do not meet our requirements, an exception will be thrown, so we still need to handle exceptions. Here is also the use of Spring Boot’s global exception handling for capture processing. For details, please refer to “SpringBoot’s Elegant Global Exception Handling”, here we simply write down the capture and processing method of parameter exceptions.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = {MethodArgumentNotValidException.class,
            ConstraintViolationException.class, BindException.class})
    public Result<String> handleValidatedException(Exception e) {
        log.error("Intercepted parameter verification failed, exception information:" + ExceptionUtil.stacktraceToString(e));
        //@RequestBody parameter verification error
        String errorMsg = "Parameter verification failed: ";
        if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;
            errorMsg = errorMsg + ex.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(", "));
            // Directly verify the specific parameters and report an error
        } else if (e instanceof ConstraintViolationException) {
            ConstraintViolationException ex = (ConstraintViolationException) e;
            errorMsg = errorMsg + ex.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(", "));
            //Validation object parameter error
        } else if (e instanceof BindException) {
            BindException ex = (BindException) e;
            errorMsg = errorMsg + ex.getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(", "));
        }
        return Result. fail(errorMsg);
    }
}
Copy Code

It can be seen that our above exception handling code captures three exceptions: MethodArgumentNotValidException.class, ConstraintViolationException.class, BindException.class respectively.

The ConstraintViolationException.class exception corresponds to the verification of the RequestParam parameter and the verification of the PathVariable parameter. If these two verifications fail, the system will throw this exception.

The BindException.class exception corresponds to the verification form submission parameters. If the verification fails, the system will throw this exception.

The MethodArgumentNotValidException.class exception corresponds to the verification of the RequestBody input parameter. If the verification fails, the system will throw this exception.

At this point, we have basically completed the verification of basic input parameters through @Validated or @Valid. Introduce the jar package, add annotation verification to the corresponding class and method, and handle the exception thrown if the verification fails, and then it can run normally. But these are just the most basic checks, and some check methods for special scenarios will be introduced below.

5. Group verification and nested verification

These two verification methods are also what we often encounter, which is also a difference between @Validated and @Valid. @Validated supports group verification, and @Valid supports nested verification.

  1. packet check

For example, we now have a parameterized UserForm, which is used in both interfaces A and B, so we normally check the parameters in this way.

@PostMapping(value = "/aaa")
public void selectAaa(@RequestBody @Validated UserForm form){
    }
    
@Data
@Accessors(chain = true)
public class UserForm {
    @NotBlank(message = "code cannot be empty")
    private String code;
    @NotBlank(message = "name cannot be empty")
    private String name;
    private String sex;
    ...
}
Copy Code

We verified the input parameter code and name in the interface selectAaa. So now if we still have an interface selectBbb, the input parameters are also received by UserForm, but we need to verify the code, name and sex for this interface. Then we need to add the @NotBlank annotation to sex. But in this case, our interface selectAaa will also check sex. This brings up the problem, the same UserForm cannot be used to receive input parameters in multiple interfaces. Is there a good solution for this? The stupid way is of course to build another UserForm2. We don’t discuss whether this is suitable or not here. Our solution is group verification.

Group verification is mainly divided into two steps. The first step is to write a group verification class.

/**
 * Validation group verification class, inherits Default, default verification without grouping
 */
public interface ValidGroup extends Default {
    interface demo extends ValidGroup {
        interface a extends demo {
        }

        interface b extends demo {

        }

        interface demo1 extends ValidGroup {

        }
    }

}
Copy Code

This group verification class can choose to inherit or not inherit the Default class. If it inherits, the non-group will also be verified by default. What does this mean? Let’s continue reading.

Going back to the interface selectAaa and selectBbb just now, selectAaa does not need to verify sex, but selectBbb needs to verify. Then we can add group verification to the sex field.

@PostMapping(value = "/bbb")
public void selectBbb(@RequestBody
@Validated(value = ValidGroup.demo.a.class) UserForm form){
    }
    
@Data
@Accessors(chain = true)
public class UserForm {
    @NotBlank(message = "code cannot be empty")
    private String code;
    @NotBlank(message = "name cannot be empty")
    private String name;
    @NotBlank(groups = ValidGroup.demo.a.class, message = "Gender cannot be empty")
    private String sex;
    ...
}
Copy Code

It can be seen that we have added the group attribute to NotBlank, and added the value attribute to the method input parameter Validated, both of which specify the same group ValidGroup.demo.a.class. In this way, the function of group verification can be played. When selectAaa enters parameters, only the code and name are verified. When SelectBbb is entered as a parameter, because a group is specified, the sex field under the same group will be verified. As for whether the code and name are verified, it is determined by whether our ValidGroup class inherits from Default. If it is inherited here, the ungrouped fields are verified by default. Then SelectBbb verifies code, name and sex. In this way, our group verification is realized, and our same receiving entity can be used on different interfaces.

  1. nested validation

Let’s look at another scenario. UserForm is still used to receive input parameters, but there is an attribute in UserForm that is interest, and interest is also an entity. There are new attributes inside, such as interest also has code and name attributes.

@Data
@Accessors(chain = true)
public class UserForm {
    @NotBlank(message = "code cannot be empty")
    private String code;
    @NotBlank(message = "name cannot be empty")
    private String name;
    @NotBlank(groups = ValidGroup.demo.a.class, message = "Gender cannot be empty")
    private String sex;
    @NotNull
    private Interest interest;
    ...
}
@Data
@Accessors(chain = true)
public class Interest {
    @NotBlank
    private String code;
    @NotBlank
    private String name;
    ...
}
Copy Code

At this time, we only use @Validated to verify the UserForm on the method input parameter, so the code, name, sex and other attribute verification are all right, but the interest attribute will have a problem, it will only verify whether the object of interest is empty , instead of verifying whether the attributes code and name inside the object meet the requirements, even if we add @NotBliank and other annotations to the fields in Interest. For example, if there is an interest that only has name=basketball, and no code is passed, it can be entered normally. Obviously this does not meet our entry requirements, so how to verify this, we need to use our nested verification here.

Nested verification is also relatively simple. On the method input parameter, use @Valid to verify UserForm. In UserForm, when we check Interest, we continue to use @Valid validation (remember what we said when we talked about the difference between @Valid and @Validated above? @Valid supports adding to fields, so here we can use @Valid). In the Interest object, we can continue to use annotations such as @NotBlank to verify specific fields.

//Nested validation
@PostMapping(value = "/bbb")
public void selectBbb(@RequestBody @Valid UserForm form){
    //xxx business code
    }
@Data
@Accessors(chain = true)
public class UserForm {
    @NotBlank(message = "code cannot be empty")
    private String code;
    @NotBlank(message = "name cannot be empty")
    private String name;
    @NotBlank(groups = ValidGroup.demo.a.class, message = "Gender cannot be empty")
    private String sex;
    
    //Add @Valid to the field here, indicating that internal attributes need to be verified, and NotNull is to verify whether the interest object exists
    @Valid
    @NotNull
    private Interest interest;
    //List format can also be verified in this way
    @Valid
    @NotNull
    private List<Interest> interest;
    ...
}
@Data
@Accessors(chain = true)
public class Interest {
    @NotBlank
    private String code;
    @NotBlank
    private String name;
    ...
}
Copy Code

In this way, we can not only verify whether the interest object is passed as a parameter, but also verify whether the code and other fields inside the interest conform to the specification. In addition, for the nested verification here, note that data binding errors may occur, you need to add the following code to the global exception to initialize the binding:

@InitBinder
private void activateDirectFieldAccess(DataBinder dataBinder) {
    dataBinder.initDirectFieldAccess();
}
Copy Code

Here is another usage scenario of nested validation. Sometimes the method input parameter is in List format, and we can also use @Validated and @Valid for nested validation. Use @Valid to check the parameters of the method, and use @NotBlank to check the specific parameters in the UserForm. Note here that you need to add @Validated to the class to take effect.

@RestController
@Validated
public class test {
        // nested validation
@PostMapping(value = "/bbb")
public void selectBbb(@RequestBody @Valid List<UserForm> formList){
    //xxx business code
    }
}
Copy Code

6. Service layer verification

As we mentioned above, there are generally two situations where we need to enter parameter verification. The first one is the method provided to the front end. The second type is the service provided to the outside world. Because if it is our own internal method, it must be entered by ourselves, so there is no need for verification. But the method provided to others cannot be guaranteed. In the first case, we will put the interface for the front end in the controller layer, that is, the various use cases we mentioned above.

But there are always special cases, such as the second one, we provide external RPC interface services directly through the service layer. Sometimes it is also necessary to perform input parameter verification in the service layer method. Here is a brief mention of the points that need to be paid attention to in the service layer. If we define both the interface and the implementation class, then the @Validated annotation needs to be added to the interface, not the implementation class. If there is only an implementation class, it is no problem to add it directly to the implementation class.

7. Conclusion

At this point, our article today is over. If you follow the article, you should be able to meet most scenarios in the project. Of course, there are still some configurations that are not mentioned here due to space issues, and you can modify and improve them according to your own usage scenarios later.

For example, during parameter verification, if the first parameter verification fails, an exception will be returned, and there is no need to verify all of them, which requires the Fast mode to be enabled. There are also verification annotations that can be used. The article does not list them one by one, and you don’t need to memorize them during use. You only need to remember a few commonly used annotations, and you can check them when you use them. Of course, if some complex logic, the annotations provided by the framework itself cannot meet the business needs, then we need to write custom annotations, implement the ConstraintValidator interface and write custom validation classes to verify.