The good ones are not as good as the ones I wrote. Hello, everyone, I’m Java class representative. A JavaWeb developer on the front line. If you want to see more wonderful articles, welcome to follow my official account, Java class representative.

The introduction

I don’t know how to write the parameter verification of controller layer in the normal business development process. Is there a direct judgment like the following?

public String add(UserVO userVO) {
    if(userVO.getAge() == null) {return "Age cannot be empty.";
    }
    if(userVO.getAge() > 120) {return "No more than 120.";
    }
    if(userVO.getName().isEmpty()){
        return "User name cannot be empty";
    }
    // omit a bunch of parameter verification...
    return "OK";
}
Copy the code

Business code has not started to write, optical parameter verification to write a bunch of judgment. There’s nothing wrong with writing this, but it comes across as unpolished and unprofessional.

The Spring framework already wraps a set of validation components for us: Validation. It is characterized by ease of use and high degree of freedom. Next, the class representative used Springboot-2.3.1. RELEASE to build a simple Web project and explained step by step how to do parameter verification gracefully in the development process.

1. Environment construction

Starting from Springboot-2.3, the verification package is independent as a starter component, so the following dependencies need to be introduced:

<! <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <! -- Web component --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId> Spring-boot-starter -web</artifactId> </dependency>Copy the code

Versions prior to SpringBoot-2.3 only needed to introduce Web dependencies.

2. Test the cat

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

public class UserVO {
    @notnull (message = "age cannot be empty ")
    private Integer age;
}
Copy the code

Then add @validated and BindingResult for receiving error information to the Controller method, and you have 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";
}
Copy the code

If the parameter does not meet the rule, the corresponding message will be returned:

Age cannot be emptyCopy the code

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

annotations Check the function
@AssertFalse Must be false
@AssertTrue It must be true
@DecimalMax Less than or equal to a given value
@DecimalMin Greater than or equal to a given value
@Digits Maximum number of integer and decimal digits can be set
@Email Verify that the Email format is correct
@Future It has to be in the future
@FutureOrPresent Present or future time
@Max The maximum
@Min The minimum value
@Negative Negative numbers (excluding 0)
@NegativeOrZero Negative or zero
@NotBlank Is not null and contains at least one non-whitespace character
@NotEmpty Not null and not null
@NotNull Not null
@Null null
@Past It has to be in the past
@PastOrPresent It has to be the past, including the present
@PositiveOrZero Positive or zero
@Size Validates the number of elements in the container

3. Normalize return values

When there are too many verification parameters, we hope to return all verification failure information at one time, which is convenient for interface callers to adjust. In this case, we need to unify the return format, and the common one is to encapsulate a result class.

public class ResultInfo<T>{
    private Integer status;
    private String message;
    private T response;
    // omit other code...
}
Copy the code

Modify the Controller method, version 2:

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."Request parameter error",collect);
}
Copy the code

When this method is requested, all error arguments are returned:

{
    "status": 400."message": "Request parameter error"."response": [
        "Must be between [1,120] years of age"."Bg field has a maximum of 3 integer bits and a maximum of 1 decimal place."."Name cannot be empty"."Email format error"]}Copy the code

4. Global exception handling

It would still be tedious to write the BindingResult information in each Controller method. Check exceptions can be handled uniformly through global exception handling.

Spring will throw an exception when we have an @validated annotation and no BindingResult. As a result, you can write a global exception handling class to handle this validation exception uniformly, eliminating the need to repeatedly organize the exception information.

The global exception handling class simply needs to annotate @RestControllerAdvice on the class and specify which exception to handle with the @ExceptionHandler annotation on the method that handles the corresponding exception.

@RestControllerAdvice
public class GlobalControllerAdvice {
    private static final String BAD_REQUEST_MSG = "Client request parameter error";
    // <1> Handle the exception thrown when the form data call fails to validate the interface
    @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> Handle the exception thrown when the JSON request body invocation interface fails to validate
    @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> Handle exceptions thrown when a single parameter fails to validate
    @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 newResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect); }}Copy the code

In fact, in the global exception handling class, we can write more than one exception handling method. The class representative summarized three kinds of exceptions that can be thrown when checking parameters:

  1. Call the interface using form Data to verify that the exception throws a BindException
  2. Using json request body call interface, check exception thrown MethodArgumentNotValidException
  3. A single parameter validation exception thrown ConstraintViolationException

Note: To verify a single parameter, add a verification comment on the parameter and mark @Validated on the class.

The global Exception handling class can add a variety of exceptions that need to be handled, such as adding an Exception. Class Exception. When all ExceptionHandler cannot handle the Exception, it will record the Exception information and return a friendly prompt.

5. Group verification

If different verification rules need to be applied to the same parameter in different scenarios, group verification is required. For example, if the newly registered user does not have a name, we allow the name field to be empty, but do not allow the name to be updated to a null character.

There are three steps for packet verification:

  1. Define a grouping class (or interface)
  2. Add to the validation annotationgroupsAttribute specifies grouping
  3. Controllermethods@ValidatedAnnotations add grouping classes
public interface Update extends Default{}Copy the code
public class UserVO {
    @notblank (message = "name cannot be blank ",groups = update.class)
    private String name;
    // omit other code...
}
Copy the code
@PostMapping("update")
public ResultInfo update(@Validated({Update.class}) UserVO userVO) {
    return new ResultInfo().success(userVO);
}
Copy the code

Careful students may have noticed that the custom Update grouping interface inherits the Default interface. Check annotations (such as: @ NotBlank) and @ validated by Default are Default. The class groups, this point in the javax.mail. Validation. Groups. The Default comments

/**
 * 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 {}Copy the code

When writing the Update grouping interface, if you inherit from Default, the following two statements are equivalent:

@Validated({Update.class})

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

The /update interface verifies not only the name field but also other fields that belong to the default. class group by Default

{
    "status": 400."message": "Client request parameter error"."response": [
        "Name cannot be empty"."Age cannot be empty."."Email cannot be empty"]}Copy the code

If Update does not inherit Default, @Validated({update. class}) verifies only the parameter fields in the update. class group.

{
    "status": 400."message": "Client request parameter error"."response": [
        "Name cannot be empty"]}Copy the code

6. Recursive check

If you add an OrderVO property to the UserVO class, and the property in OrderVO also needs to be validated, you can use recursive validation by adding the @VALID annotation to the property (also for collections).

OrderVO classes as follows

public class OrderVO {
    @NotNull
    private Long id;
    @notblank (message = "itemName cannot be empty ")
    private String itemName;
    // omit other code...
}
Copy the code

Add an OrderVO type property to the UserVO class

public class UserVO {
    @notblank (message = "name cannot be blank ",groups = update.class)
    private String name;
	// OrderVO needs recursive validation
    @Valid
    private OrderVO orderVO;
    // omit other code...
}   

Copy the code

Call request validation is as follows:

7. Customize the verification

Validation from Spring provides so many features that it can be used for most parameter validation scenarios in everyday development. However, a good framework is always easy to extend. With the ability to scale, you can deal with more complex business scenarios; after all, the only constant in development is change itself.

Spring Validation allows you to customize Validation. It is simple to implement in two steps:

  1. Custom validation annotations
  2. Write the validator class

The code is also very simple, combined with comments you can read it

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {HaveNoBlankValidator.class})// Specify which class performs the validation logic
public @interface HaveNoBlank {
	
    // The message returned by default when a verification error occurs
    String message(a) default"No Spaces in the string"; Class<? >[] groups()default{}; Class<? extends Payload>[] payload()default{};/** * use */ when multiple annotations are specified on the same element
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interfaceList { NotBlank[] value(); }}Copy the code
public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank.String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // null does not verify
        if (value == null) {
            return true;
        }
        if (value.contains("")) {
            // Verification failed
            return false;
        }
        // The verification succeeds
        return true; }}Copy the code

Custom validation annotations are used in the same way as the built-in annotations. You can add the corresponding annotations to the required fields and verify them by yourself

review

That’s all about using Spring Validation to gracefully validate parameters. Let’s summarize the Validation features mentioned in this article

  1. Built-in a variety of common verification annotations
  2. Supports verification of a single parameter
  3. Automatic assembly of validation exceptions with global exception handling
  4. Packet check
  5. Recursive verification is supported
  6. Custom check

The code has been uploaded to GitHub


【 Previous recommendation 】

MySQL priority queue (order by limit)

Dubbo exception handling source code exploration and best practices

Freemarker Tutorial (I)- Template development Manual

RabbitMQ official tutorial translation


👇 code word is not easy, follow the Java class representative, get the latest Java dry goods 👇