Use Spring Validation to perform parameter validation gracefully

introduction

I don't know how you write the parameter verification of the controller layer in the usual business development process? Is there a direct judgment like the following?

public String add(UserVO userVO) {
    if(userVO.getAge() == null){
        return "年龄不能为空";
    }
    if(userVO.getAge() > 120){
        return "年龄不能超过120";
    }
    if(userVO.getName().isEmpty()){
        return "用户名不能为空";
    }
    // 省略一堆参数校验...
    return "OK";
}

The business code hasn't been written yet, and a lot of judgments have been written for optical parameter verification. Although there is nothing wrong with writing this way, it gives people the feeling that it is not elegant and professional.

In fact, the Springframework has already encapsulated a set of verification components for us: validation. Its characteristics are easy to use and high degree of freedom. The next lesson represents the use of springboot-2.3.1.RELEASEbuilding a simple Web project, and I will explain step by step how to elegantly check the parameters during the development process.

1. Environment Setup

From the springboot-2.3beginning, the verification package has been independent as a startercomponent, so the following dependencies need to be introduced:

<!--校验组件-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--web组件-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

The springboot-2.3previous version only needs to introduce web dependencies.

2. Small test

Parameter verification is very simple, first add a verification rule annotation on the field to be verified

public class UserVO {
    @NotNull(message = "age 不能为空")
    private Integer age;
}

Then controlleradd in the method @Validatedand used to receive the error message BindingResult, so there is the first version:

public String add1(@Validated UserVO userVO, BindingResult result) {
    List<FieldError> fieldErrors = result.getFieldErrors();
    if(!fieldErrors.isEmpty()){
        return fieldErrors.get(0).getDefaultMessage();
    }
    return "OK";
}

Use the tool (postman) to request the interface. If the parameters do not meet the rules, the corresponding messageinformation will be returned:

age 不能为空

There are many built-in verification annotations, listed as follows:

annotation Check function
@AssertFalse Must be false
@AssertTrue Must be true
@DecimalMax Less than or equal to the given value
@DecimalMin Greater than or equal to the given value
@Digits The maximum number of integers and the maximum number of decimals can be set
@Email Check whether it conforms to Email format
@Future Must be in the future
@FutureOrPresent Current or future time
@Max Max
@Min Minimum
@Negative Negative numbers (not including 0)
@NegativeOrZero Negative or 0
@NotBlank Is not null and contains at least one non-whitespace character
@NotEmpty Not null and not empty
@NotNull Not null
@Null Is null
@Past Must be in the past
@PastOrPresent Must be in the past, including the present
@PositiveOrZero Positive or 0
@Size Check the number of elements in the container

3. Standard return value

After there are more parameters to be verified, we hope to return all verification failure information at one time to facilitate interface callers to adjust. This requires a unified return format. The common one is to encapsulate a result class.

public class ResultInfo<T>{
    private Integer status;
    private String message;
    private T response;
    // 省略其他代码...
}

Modify the controllermethod, second edition:

public ResultInfo add2(@Validated UserVO userVO, BindingResult result) {
    List<FieldError> fieldErrors = result.getFieldErrors();
    List<String> collect = fieldErrors.stream()
            .map(o -> o.getDefaultMessage())
            .collect(Collectors.toList());
    return new ResultInfo<>().success(400,"请求参数错误",collect);
}

When this method is requested, all the error parameters are returned:

{
    "status": 400,
    "message": "请求参数错误",
    "response": [
        "年龄必须在[1,120]之间",
        "bg 字段的整数位最多为3位,小数位最多为1位",
        "name 不能为空",
        "email 格式错误"
    ]
}

4. Global exception handling

ControllerIf you write the BindingResultinformation processing in each method, it is still very cumbersome to use. The check exception can be handled uniformly through global exception handling.

When we write a @validatedcomment and don't write BindingResultit, Spring will throw an exception. As a result, a global exception handling class can be written to uniformly handle this verification exception, thereby eliminating the need to repeat the code for organizing exception information.

The global exception handling class only needs to be marked on the class @RestControllerAdvice, and @ExceptionHandlerannotations are used on the corresponding exception handling methods to specify which exception to handle.

@RestControllerAdvice
public class GlobalControllerAdvice {
    private static final String BAD_REQUEST_MSG = "客户端请求参数错误";
    // <1> 处理 form data方式调用接口校验失败抛出的异常 
    @ExceptionHandler(BindException.class)
    public ResultInfo bindExceptionHandler(BindException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> collect = fieldErrors.stream()
                .map(o -> o.getDefaultMessage())
                .collect(Collectors.toList());
        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
    }
    // <2> 处理 json 请求体调用接口校验失败抛出的异常 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultInfo methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> collect = fieldErrors.stream()
                .map(o -> o.getDefaultMessage())
                .collect(Collectors.toList());
        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
    }
    // <3> 处理单个参数校验失败抛出的异常
    @ExceptionHandler(ConstraintViolationException.class)
    public ResultInfo constraintViolationExceptionHandler(ConstraintViolationException e) {
        Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
        List<String> collect = constraintViolations.stream()
                .map(o -> o.getMessage())
                .collect(Collectors.toList());
        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
    }
    

}

In fact, in the global exception handling class, we can write multiple exception handling methods. The class representative summarized the exceptions that may be thrown during three parameter verification:

  1. Use the form data method to call the interface, and the verification exception throws BindException

  2. Use the json request body to call the interface, and the validation exception throws MethodArgumentNotValidException

  3. Single parameter verification exception throws ConstraintViolationException

Note: Single parameter verification needs to add verification notes to the parameters and mark them on the class @Validated.

The global exception handling class can add various exceptions that need to be handled, such as adding a pair Exception.classof exception handling. When all of ExceptionHandlerthem cannot be handled, it records the exception information and returns a friendly prompt.

5. Group check

If you need to apply different verification rules in different scenarios for the same parameter, you need to use packet verification. For example: the newly registered user has not given a name, we allow the namefield to be empty, but it is not allowed to update the name to empty characters.

There are three steps to group verification:

  1. Define a grouping class (or interface)

  2. Add groupsattribute specified grouping on verification annotation

  3. Controller@ValidatedAdd grouping class to method annotation

public interface Update extends Default{
}
public class UserVO {
    @NotBlank(message = "name 不能为空",groups = Update.class)
    private String name;
    // 省略其他代码...
}
@PostMapping("update")
public ResultInfo update(@Validated({Update.class}) UserVO userVO) {
    return new ResultInfo().success(userVO);
}

Attentive students may have noticed that the custom Updategroup interface inherits the Defaultinterface. The verification annotations (such as:) @NotBlankand the @validateddefault belong to the Default.classgrouping, which is javax.validation.groups.Defaultexplained in the annotations

/**
 * Default Jakarta Bean Validation group.
 * <p>
 * Unless a list of groups is explicitly defined:
 * <ul>
 *     <li>constraints belong to the {@code Default} group</li>
 *     <li>validation applies to the {@code Default} group</li>
 * </ul>
 * Most structural constraints should belong to the default group.
 *
 * @author Emmanuel Bernard
 */
public interface Default {
}

When writing a Updatepacket interface, if inherited Default, the following two writings are equivalent:

@Validated({Update.class})

@Validated({Update.class,Default.class})

Request the /updateinterface to see that not only the namefields are verified, but other fields that belong to the Default.classgroup by default are also verified

{
    "status": 400,
    "message": "客户端请求参数错误",
    "response": [
        "name 不能为空",
        "age 不能为空",
        "email 不能为空"
    ]
}

If it is Updatenot inherited Default, @Validated({Update.class})only Update.classthe parameter fields belonging to the group will be checked . After the modification, the interface is requested again to get the following results. You can see that other fields are not involved in the check:

{
    "status": 400,
    "message": "客户端请求参数错误",
    "response": [
        "name 不能为空"
    ]
}

6. Recursive verification

If an attribute of the OrderVO class is added to the UserVO class, and the attributes in the OrderVO also need to be verified, recursive verification is used, which @Validcan be achieved by adding annotations to the corresponding properties (the same applies to collections)

The OrderVO class is as follows

public class OrderVO {
    @NotNull
    private Long id;
    @NotBlank(message = "itemName 不能为空")
    private String itemName;
    // 省略其他代码...
}

Add an attribute of type OrderVO to the UserVO class

public class UserVO {
    @NotBlank(message = "name 不能为空",groups = Update.class)
    private String name;
    //需要递归校验的OrderVO
    @Valid
    private OrderVO orderVO;
    // 省略其他代码...
}   

The call request verification is as follows:

7. Custom check

Spring's validation provides us with so many features, which can almost meet most of the parameter verification scenarios in daily development. However, a good framework must be easy to extend. With scalability, you can deal with more complex business scenarios. After all, in the development process, the only constant is the change itself .

Spring Validation allows users to customize the validation, the implementation is very simple, in two steps:

  1. Custom check annotation

  2. Write verifier class

The code is also very simple, you can understand it at a glance with the comments

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {HaveNoBlankValidator.class})// 标明由哪个类执行校验逻辑
public @interface HaveNoBlank {
 
    // 校验出错时默认返回的消息
    String message() default "字符串中不能含有空格";

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

    Class<? extends Payload>[] payload() default { };

    /**
     * 同一个元素上指定多个该注解时使用
     */
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        NotBlank[] value();
    }
}
public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // null 不做检验
        if (value == null) {
            return true;
        }
        if (value.contains(" ")) {
            // 校验失败
            return false;
        }
        // 校验成功
        return true;
    }
}

The custom verification annotations are the same as the built-in annotations. Just add the corresponding annotations on the required fields, and students can verify by themselves

review

The above is the whole content of how to use Spring Validation to verify the parameters elegantly. The following focuses on the verification features mentioned in the article

  1. Built-in a variety of common verification notes

  2. Support single parameter verification

  3. Combined with global exception handling to automatically assemble and verify exceptions

  4. Packet check

  5. Support recursive check

  6. Custom check

There is no way, but the technique can be achieved; if there is no way, it ends with the technique

Welcome everyone to follow the Java Way public account

Good article, I am reading ❤️

Guess you like

Origin blog.csdn.net/hollis_chuang/article/details/108687966