Elegant parameter verification in Spring/Spring Boot

Posted by ppowell on Thu, 10 Feb 2022 17:29:19 +0100

Recently, when writing a project, every controller is filled with a large number of if to verify parameters, and each controller is different, so the verification method cannot be encapsulated Especially troublesome

The most common approach is like this. We check each parameter of the request one by one through the if/else statement.

				if (
                        StringUtils.isBlank(dto.getFwsz().get(i).getFwmc()) ||
                                dto.getFwsz().get(i).getCsl() == null ||
                                dto.getFwsz().get(i).getLjzcsl() == null ||
                                dto.getFwsz().get(i).getYlzcsl() == null)
                {
                    throw new APIException("Required items are missing");
                }

Such code clearly violates the principle of single responsibility. A large number of non business codes are mixed in business codes, which are very difficult to maintain, and will also lead to miscellaneous business layer codes!

The following will demonstrate how to carry out parameter verification gracefully in the SpringBoot program through an example program (ordinary Java programs are also applicable).

@PostMapping
    public ResponseEntity<PersonRequest> save(@RequestBody @Valid PersonRequest personRequest) {
        return ResponseEntity.ok().body(personRequest);
    }
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PersonRequest {

    @NotNull(message = "classId Cannot be empty")
    private String classId;

    @Size(max = 33)
    @NotNull(message = "name Cannot be empty")
    private String name;

    @Pattern(regexp = "(^Man$|^Woman$|^UGM$)", message = "sex The value is not in the optional range")
    @NotNull(message = "sex Cannot be empty")
    private String sex;

}

Regular expression Description:

  • ^String: matches a string that starts with a string
  • String $: matches a string ending in a string
  • ^String $: exactly match string
  • (Man$|^Woman$|UGM $): the value can only be selected from man, woman and UGM

By default, Spring will convert this exception to HTTP status 400 (bad request) However, this return is unfriendly. Custom exception handlers can help us catch exceptions and do some simple processing.

@ControllerAdvice(assignableTypes = {PersonController.class})
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
    }
}
Validation request parameters

The validation Request Parameters (Path Variables and Request Parameters) are the method parameters marked by @ pathvariables and @ requestparameters.

PersonController

Don't forget to add the Validated annotation on the class. This parameter can tell Spring to verify the method parameters.

@RestController
@RequestMapping("/api/persons")
@Validated
public class PersonController {

    @GetMapping("/{id}")
    public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5, message = "exceed id The scope of") Integer id) {
        return ResponseEntity.ok().body(id);
    }

    @PutMapping
    public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6, message = "exceed name The scope of") String name) {
        return ResponseEntity.ok().body(name);
    }
}

ExceptionHandler

  @ExceptionHandler(ConstraintViolationException.class)
  ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
     return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
  }
Validate methods in Service

We can also verify the input of any Spring Bean, not just the Controller level input. This requirement can be achieved by using a combination of @ Validated and @ Valid annotations!

In general, we prefer to use this scheme in the project.

Don't forget to add the Validated annotation on the class. This parameter can tell Spring to verify the method parameters.

@Service
@Validated
public class PersonService {

    public void validatePersonRequest(@Valid PersonRequest personRequest) {
        // do something
    }

}
Validator manually validates parameters programmatically

In some scenarios, we may need to manually verify and obtain the verification results.

The Validator example we get from the Validator factory class. In addition, if it is in Spring Bean, it can also be injected directly through @ Autowired.

@Autowired
Validator validate

The specific usage is as follows:

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator()
PersonRequest personRequest = PersonRequest.builder().sex("Man22")
  .classId("82938390").build();
Set<ConstraintViolation<PersonRequest>> violations = validator.validate(personRequest);
// Output exception information
violations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
}

The output results are as follows:

sex The value is not in the optional range
name Cannot be empty
Customize validator (Practical)

If the built-in verification annotation cannot meet your needs, you can also customize the implementation annotation.

Case 1: check whether the value of a specific field is in the optional range
For example, we now have a requirement: the PersonRequest class has an additional Region field, and the Region field can only be one of China, China Taiwan and China Hong Kong.

First, you need to create an annotation Region.

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = RegionValidator.class)
@Documented
public @interface Region {

    String message() default "Region The value is not in the optional range";

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

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

In the second step, you need to implement the ConstraintValidator interface and override the isValid method.

public class RegionValidator implements ConstraintValidator<Region, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        HashSet<Object> regions = new HashSet<>();
        regions.add("China");
        regions.add("China-Taiwan");
        regions.add("China-HongKong");
        return regions.contains(value);
    }
}

Now you can use this annotation:

@Region
private String region;
Case 2: check the phone number

Check whether our phone number is legal. This can be done through regular expressions. Relevant regular expressions can be found on the Internet. You can even search for regular expressions for specific operator phone number segments.

PhoneNumber.java


Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
public @interface PhoneNumber {
    String message() default "Invalid phone number";
    Class[] groups() default {};
    Class[] payload() default {};
}

PhoneNumberValidator.java

public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {

    @Override
    public boolean isValid(String phoneField, ConstraintValidatorContext context) {
        if (phoneField == null) {
            // can be null
            return true;
        }
        //  11 digits of mobile phone number in mainland China, matching format: fixed format of the first three digits + any number of the last 8 digits
        // ^Matches the beginning of the input string
        // \d matches one or more numbers, where \ is to be escaped, so it is \ \ d
        // $matches the position at the end of the input string
        String regExp = "^[1]((3[0-9])|(4[5-9])|(5[0-3,5-9])|([6][5,6])|(7[0-9])|(8[0-9])|(9[1,8,9]))\\d{8}$";
        return phoneField.matches(regExp);
    }
}

Done, we can use this annotation now.

@PhoneNumber(message = "phoneNumber Incorrect format")
@NotNull(message = "phoneNumber Cannot be empty")
private String phoneNumber;

Summary of common verification comments
JSR303 defines the standard validation API of Bean Validation and does not provide an implementation. Hibernate Validation is the implementation of this specification / specification, hibernate validator, and @ Email, @ Length, @ Range and other annotations are added. The underlying dependency of Spring Validation is Hibernate Validation.

Verification comments provided by JSR:

  • @Null the annotated element must be null
  • @NotNull the annotated element must not be null
  • @AssertTrue the annotated element must be true
  • @AssertFalse the annotated element must be false
  • @Min(value) the annotated element must be a number and its value must be greater than or equal to the specified minimum value
  • @Max(value) the annotated element must be a number whose value must be less than or equal to the specified maximum value
  • @DecimalMin(value) the annotated element must be a number whose value must be greater than or equal to the specified minimum value
  • @DecimalMax(value) the annotated element must be a number whose value must be less than or equal to the specified maximum value
  • @Size(max=, min =) the size of the annotated element must be within the specified range
  • @Digits (integer, fraction) the annotated element must be a number and its value must be within an acceptable range
  • @Past annotated element must be a past date
  • @Future the annotated element must be a future date
  • @Pattern(regex=,flag =) the annotated element must conform to the specified regular expression

Verification comments provided by Hibernate Validator:

  • @NotBlank(message =) validation string is not null and must be longer than 0
  • @The annotated element of Email must be an Email address
  • @Length(min=,max =) the size of the annotated string must be within the specified range
  • @NotEmpty the of the annotated string must be non empty
  • @Range(min=,max=,message =) the annotated element must be within the appropriate range
    expand

@What is the difference between NotNull and @ Column(nullable = false)?

  • @NotNull is a JSR 303 Bean validation annotation, which has nothing to do with the database constraint itself.
  • @Column(nullable = false): it is a method that JPA declares the column to be non empty.
    To sum up, the former is used for validation, while the latter is used to indicate the constraints on the table when the database creates the table.

Topics: Java