SpringBoot 2.0 uses declarative annotations to simplify parameter verification

1. First look at the final effect

When we initiate a POST /users request expecting to add a new user

@PostMapping("/users")
public void addUser(@Valid @RequestBody User user) {
    log.info("User added successfully:{}", user);
}

Suppose to carry the following JSON data as request parameters, but usually we would expect username and password to not be empty

{
        "username":"",
        "password":""
}

Therefore, we expect to get a specific response that tells us the number of parameter validation failures and the reason.

{
    "code": 400,
    "message": "BAD_REQUEST",
    "data": "Parameter verification error (2): username cannot be empty; password cannot be empty"
}

(2) in data indicates that there are two parameter verification failures and then indicates the reason for the verification failure

 

2. Next, the implementation process will be explained

A total of three steps

 

The first step is to add a declarative annotation to the parameter to define the type of validation required by the parameter, such as the User object above

@Data
public class User {
    private String id;
    @NotBlank(message = "Username cannot be empty")
    private String username;
    @NotBlank(message = "Password cannot be blank")
    private String password;
    @Email(message = "Email format error")
    private String email;
    @PastOrPresent(message = "The date is less than or equal to the current time")
    private Date birthday;
    @Pattern(regexp = "^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(17[013678])|(18[0,5-9]))\\d{8}$"
            , message = "Incorrect phone number format")
    private String phone;
    @Min(value = 0, message = "Age is out of range, the minimum value is 0")
    @Max(value = 120, message = "Age is out of range, the maximum value is 120")
    private Integer age;
}
annotation describe
@AssertFalse The annotated element must be of type Boolean and the value is false
@AssertTrue The annotated element must be of type Boolean and the value is true
@DecimalMax The annotated element must be a number and the value is less than or equal to the given value
@DecimalMin The annotated element must be a number and the value is greater than or equal to the given value
@Digits The annotated element must be a number and the value must be the specified number of digits
@Future The annotated element must be a date in the future
@Max The annotated element must be a number and the value is less than or equal to the given value
@Min The annotated element must be a number and the value is less than or equal to the given value
@Range The annotated element must be within the specified range
@NotNull The value of the annotated element cannot be null
@NotBlank The annotated element value has content
@Null The value of the annotated element is null
@Past The annotated element must be a date in the past
@PastOrPresent The annotated element must be a date in the past or present
@Pattern The annotated element must satisfy the given regular expression
@Size The annotated element must be a String, a collection or an array, and the length and size must be within the given range
@Email The annotated element must meet the Email format

Notice:

a. Among them, the username and password attributes must be passed and other attributes are not restricted

b. The message attribute in the annotation will be assigned to the defaultMessage attribute when the validation fails and an exception is thrown

 

The second step is to add the @Valid annotation before the parameters that need to be verified

public void addUser(@Valid @RequestBody User user)

Notice

a. If the @Valid annotation is not added, the parameters will not be verified

 

The third step, add the WebExchangeBindException exception handler

@ Slf4j
@RestControllerAdvice
public class MyExceptionHandler {
    @ExceptionHandler(WebExchangeBindException.class)
    public Map<String, Object> handle(WebExchangeBindException exception) {
        //Get the set of parameter validation errors
        List<FieldError> fieldErrors = exception.getFieldErrors();
        //Formatted to provide friendly error prompts
        String data = String.format("Parameter verification error (%s): %s", fieldErrors.size(),
                fieldErrors.stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining(";")));
        //Parameter verification failure response failure number and reason
        return ImmutableMap.of("code", exception.getStatus().value(),
                "message", exception.getStatus(),
                "data", data);
    }
}

@RestControllerAdvice @ResponseBody is combined with the @ControllerAdvice annotations, indicating that the current class is a controller enhancement class, which is usually paired with the @ExceptionHandler annotation to capture and handle exceptions

@ResponseBody Indicates that message conversion is used instead of content negotiation when responding to the client. Jackson is used for parsing by default. All methods annotated on the class under the table name class need to respond to JSON (no other messages are used) converter)

The method annotated with @ExceptionHandler will capture and handle the specified exception, and the WebExchangeBindException exception is handled in this article

The API provided by ImmutableMap.of() Guava needs to introduce the following dependencies

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>

 

Above, you can achieve the effect at the beginning of this article. But there is still room for improvement. For example, the validation annotation of the age property of the User object can be simplified to

@Range(min = 0, max = 120, message = "Age greater than or equal to 0 and less than or equal to 120")
private Integer age;

At the same time, we can use a custom parameter validator to implement a general mobile phone number validation annotation.

 

3. Implement custom parameter validation annotations and validators

In the above, we can find that if we want to check whether the mobile phone number conforms to the format, we need to add a long string of regular expressions to the annotation. Let's use a custom parameter verification annotation and a custom parameter validator to achieve this. A general mobile phone number verification rule. Similarly, divided into three steps

 

The first step is to define a parameter validation annotation, copy the @NotNull annotation and modify it

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,
        ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
//Define which parameter validator to use for the current annotation
@Constraint(validatedBy = PhoneValidator.class)
@Repeatable(Phone.List.class)
public @interface Phone {
    String message() default "Mobile phone number format error";

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

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

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,
            ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        Phone[] value();
    }
}

Notice:

a.message, groups, and payload attributes need to be defined in the parameter verification annotation and cannot be default\

b. @Repeatable is a meta-annotation in JDK1.8, which means repeating the same annotation in the same position

If the JDK version used is lower than 1.8, the @Phone annotation can be created in the following way

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,
        ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
    String message() default "Mobile phone number format error";

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

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

 

Next, you need to define the PhoneValidator parameter validator above

public class PhoneValidator implements ConstraintValidator<Phone, Object> {
    private static final String PHONE_REGEX = "^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(17[013678])|(18[0,5-9]))\\d{8}$";

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
        //Return true if the value is not empty or if the regular expression is satisfied
        return Objects.isNull(value) || Pattern.compile(PHONE_REGEX).matcher(value.toString()).find();
    }
}

 

Finally, use the parameter verification annotation to replace the cumbersome @Pattern annotation above

@Phone
private String phone;

 

test

 

Advantages of using custom parameter validation

1. Eliminate coupling, if someday you need to change the regex you need to make changes in every referenced place

2. It is easy to understand and can clearly indicate that this is a mobile phone verification annotation. Even if the code reader is not familiar with regular expressions, they can guess what this annotation is used for.

3. More powerful, custom validation logic, can do more things, and even introduce other components in the validator, such as using @Autowired to introduce service classes for processing validation judgment

 

Finally, post the pom.xml file of this article

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>23.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324407919&siteId=291194637