Data verification is usually done in the coding process, in each layer of the system may be to achieve some verification logic, and then to do business processing. These tedious verification and our business code in a piece will appear bloated. And these checks are usually business independent. I also used Hibernate Validator in my work, but I found that someone didn’t use it well (even see some if else validation code…). So here I decided to clean up the use of Hibernate Validator

Bean Validation 2.0 (JSR 380) defines the metadata model and API for entity and method Validation. Hibernate Validator is the best implementation so far

Use of Hibernate Validator

Rely on

If it is a Spring Boot project, then the Spring-boot-starter -web already relies on hibernate-Validator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
Copy the code

In the case of Spring Mvc, you can add hibernate-Validator dependencies directly

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.17. The Final</version>
</dependency>
Copy the code

Bean constraint declaration and validation, Validator

Start by adding constraint annotations to our Java objects

@Data
@AllArgsConstructor
public class User {

    private String id;

    @NotBlank
    @Size(max = 20)
    private String name;

    @NotNull
    @Pattern(regexp = "[A-Z][a-z][0-9]")
    private String password;
    
    @NotNull
    private Integer age;

    @Max(10)
    @Min(1)
    private Integer level;
}
Copy the code

To validate the entity instance, you need to obtain the Validator instance first

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
Copy the code

The Validator interface has three methods that can be used to validate the entire entity or just a single attribute of the entity

  • Validator#validate()Verify all constraints for all beans
  • Validator#validateProperty()Validate a single attribute
  • Validator#validateValue()Checks whether a single attribute of a given class can be successfully validated
public class UserTest {

    private static Validator validator;

    @BeforeAll
    public static void setUpValidator(a) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @Test
    public void validatorTest(a) {
        User user = new User(null."".! "" @ # $".null.11);

        // Verify all constraints on all beans
        Set<ConstraintViolation<User>> constraintViolations = validator.validate(user);
        // Verify a single attribute
        Set<ConstraintViolation<User>> constraintViolations2 = validator.validateProperty(user, "name");
        // Check whether a single attribute of a given class can be successfully validated
        Set<ConstraintViolation<User>> constraintViolations3 = validator.validateValue(User.class, "password"."sa!"); constraintViolations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); constraintViolations2.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); constraintViolations3.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); }}Copy the code

The test results

The value cannot be empty. Maximum value: 10 Match regular expressions"[A-Z][a-z][0-9]"Cannot be null Cannot be null must match a regular expression"[A-Z][a-z][0-9]"
Copy the code

Method constraint declaration and validation, ExecutableValidator

Starting with Bean Validation 1.1, constraints can be applied not only to Javabeans and their properties, but also to the parameters and return values of methods and constructors of any Java type. Here is a brief example

public class RentalStation {

    public RentalStation(@NotNull String name) {
        / /...
    }

    public void rentCar(@NotNull @Future LocalDate startDate, @Min(1) int durationInDays) {
        / /...
    }

    @NotNull
    @Size(min = 1)
    public List<@NotNull String> getCustomers() {
        / /...
        return null; }}Copy the code

The ExecutableValidator interface performs the validation of method constraints

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
executableValidator = factory.getValidator().forExecutables();
Copy the code

The ExecutableValidator interface has four methods:

  • validateParameters()andvalidateReturnValue()For method validation
  • validateConstructorParameters()andvalidateConstructorReturnValue()Used for constructor validation
public class RentalStationTest {

    private static ExecutableValidator executableValidator;

    @BeforeAll
    public static void setUpValidator(a) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        executableValidator = factory.getValidator().forExecutables();
    }

    @Test
    public void validatorTest(a) throws NoSuchMethodException {
        RentalStation rentalStation = new RentalStation("z");

        Method method = RentalStation.class.getMethod("rentCar", LocalDate.class, int.class);
        Object[] parameterValues = {LocalDate.now().minusDays(1), 0}; Set<ConstraintViolation<RentalStation>> violations = executableValidator.validateParameters( rentalStation, method, parameterValues); violations.forEach(violation -> System.out.println(violation.getMessage())); }}Copy the code

The test results

It needs to be a future time not less than 1Copy the code

Constraint annotations

There are 22 constraint annotations for Validator-api-2.0, as shown in the table below

Empty and non-empty checks

annotations Support for Java types instructions
@Null Object null
@NotNull Object Not null
@NotBlank CharSequence It is not null and must have a non-space character
@NotEmpty CharSequence, Collection, Map, and Array Not null and not null (length/size>0)

Boolean value to check

annotations Support for Java types instructions note
@AssertTrue Boolean, Boolean To true Null effectively
@AssertFalse Boolean, Boolean To false Null effectively

Date of inspection

annotations Support for Java types instructions note
@Future Date, Calendar, Instant, LocalDate, LocalDateTime, LocalTime, MonthDay, OffsetDateTime, OffsetTime, Year, YearMonth, ZonedDateTime, HijrahDate, JapaneseDate, MinguoDate, ThaiBuddhistDate Verify that the date is after the current time Null effectively
@FutureOrPresent Date, Calendar, Instant, LocalDate, LocalDateTime, LocalTime, MonthDay, OffsetDateTime, OffsetTime, Year, YearMonth, ZonedDateTime, HijrahDate, JapaneseDate, MinguoDate, ThaiBuddhistDate Verify the date is the current time or later Null effectively
@Past Date, Calendar, Instant, LocalDate, LocalDateTime, LocalTime, MonthDay, OffsetDateTime, OffsetTime, Year, YearMonth, ZonedDateTime, HijrahDate, JapaneseDate, MinguoDate, ThaiBuddhistDate Verify that the date is before the current time Null effectively
@PastOrPresent Date, Calendar, Instant, LocalDate, LocalDateTime, LocalTime, MonthDay, OffsetDateTime, OffsetTime, Year, YearMonth, ZonedDateTime, HijrahDate, JapaneseDate, MinguoDate, ThaiBuddhistDate Verify that the date is the current time or before Null effectively

Numerical check

annotations Support for Java types instructions note
@Max BigDecimal, BigInteger, Byte, short, int, long, and wrapper classes Less than or equal to Null effectively
@Min BigDecimal, BigInteger, Byte, short, int, long, and wrapper classes Greater than or equal to Null effectively
@DecimalMax BigDecimal, BigInteger, CharSequence, byte, short, int, long, and wrapper classes Less than or equal to Null effectively
@DecimalMin BigDecimal, BigInteger, CharSequence, byte, short, int, long, and wrapper classes Greater than or equal to Null effectively
@Negative BigDecimal, BigInteger, byte, short, int, long, float, double, and wrapper classes A negative number Null is valid, and 0 is invalid
@NegativeOrZero BigDecimal, BigInteger, byte, short, int, long, float, double, and wrapper classes Negative or zero Null effectively
@Positive BigDecimal, BigInteger, byte, short, int, long, float, double, and wrapper classes A positive number Null is valid, and 0 is invalid
@PositiveOrZero BigDecimal, BigInteger, byte, short, int, long, float, double, and wrapper classes Positive or zero Null effectively
@Digits(integer = 3, fraction = 2) BigDecimal, BigInteger, CharSequence, byte, short, int, long, and wrapper classes The upper limit of integer and decimal places Null effectively

other

annotations Support for Java types instructions note
@Pattern CharSequence Matches the specified regular expression Null effectively
@Email CharSequence Email address Valid for NULL, default re'*'
@Size CharSequence, Collection, Map, and Array Size range (length/size>0) Null effectively

Hibernate – Validator Extension Constraints (part)

annotations Support for Java types instructions
@Length String String length range
@Range Value type and String Specified range
@URL URL address verification

Custom constraint annotations

In addition to the constraint annotations provided above (which can be satisfied in most cases), you can customize your own constraint annotations to suit your own needs

There are three steps to defining a custom constraint

  • Create constraint annotations
  • Implement a validator
  • Define the default error message

So let’s go ahead and define a simple annotation to verify the phone number

@Documented @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Constraint(validatedBy = {MobileValidator.class}) @Retention(RUNTIME) @Repeatable(Mobile.List.class) public @interface Mobile {/** * error message() default "Mobile phone number is incorrect "; Class<? >[] groups() default {}; Class<? extends Payload>[] payload() default {}; String regexp () the default "^ 1 ([38] [0-9] 4 [579] | | 5 [0, 3, 5-9] 6 [6] | | 7 [0135678] [89] | 9) \ \ d {8} $"; @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @Documented @interface List { Mobile[] value(); }}Copy the code

Leaving aside the configuration of annotations, custom constraints require the following three properties

  • messageError message, can write dead, can also fill in the international key
  • groupsGrouping information that allows you to specify the validation group to which this constraint belongs (grouping constraints are covered below)
  • payloadPayload. You can use the payload to mark operations that require special treatment

The @REPEATable annotation and the List definition allow the annotation to be repeated multiple times in the same location, usually with different configurations (such as different groups and messages)

@constraint (validatedBy = {mobilevalidator.class}) is the validator that specifies our custom Constraint. Need to implement javax.mail. Validation. ConstraintValidator interface

public class MobileValidator implements ConstraintValidator<Mobile.String> {

    /** * Mobile authentication rules */
    private Pattern pattern;

    @Override
    public void initialize(Mobile mobile) {
        pattern = Pattern.compile(mobile.regexp());
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        returnpattern.matcher(value).matches(); }}Copy the code

The ConstraintValidator interface defines two type parameters that are set in the implementation. The first specifies the annotation class to validate (such as Mobile), and the second specifies the type of element the validator can handle (such as String). The initialize() method accesses the property value of the constraint annotation; The isValid() method is used for validation and returns true to indicate that validation has passed

The Bean validation specification recommends that null values be considered valid. If NULL is not a valid value for the element, the @notnull explicit annotation should be used

So now we have our custom constraint, and we can test it with an example

public class MobileTest {

    public void setMobile(@Mobile String mobile){
        // to do
    }

    private static ExecutableValidator executableValidator;

    @BeforeAll
    public static void setUpValidator(a) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        executableValidator = factory.getValidator().forExecutables();
    }

    @Test
    public void manufacturerIsNull(a) throws NoSuchMethodException {
        MobileTest mobileTest = new MobileTest();

        Method method = MobileTest.class.getMethod("setMobile", String.class);
        Object[] parameterValues = {"1111111"}; Set<ConstraintViolation<MobileTest>> violations = executableValidator.validateParameters( mobileTest, method, parameterValues); violations.forEach(violation -> System.out.println(violation.getMessage())); }}Copy the code
The mobile phone number is incorrectCopy the code

Grouping constraints

In the custom constraints above, there is a grouping groups attribute is used to specify the validation constraints, we add annotations at as attributes, if there is no grouping configuration information, then the Default will be the Default group javax.mail. Validation. Groups. The Default

The group is defined by the interface and is used as the identifier. Here we create two identifiers, AddGroup and UpdateGroup, to identify new additions and modifications, respectively

public interface AddGroup {
}

public interface UpdateGroup {
}
Copy the code

We then group the ID attribute of our User object

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    @Null(groups = AddGroup.class)
    @NotBlank(groups = UpdateGroup.class)
    private String id;
    
    / /... Other properties are omitted
}    
Copy the code

Let’s see how it works. Okay

@Test
public void validatorGroupTest() { User user = new User(); Set<ConstraintViolation<User>> constraintViolations = validator.validatevalue (user.class,"id"."", UpdateGroup.class);
    Set<ConstraintViolation<User>> constraintViolations2 = validator.validateValue(User.class, "id"."");
    Set<ConstraintViolation<User>> constraintViolations3 = validator.validateValue(User.class, "id"."", AddGroup.class);

	constraintViolations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
	constraintViolations2.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
	constraintViolations3.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
}
Copy the code

The above test validates only when the UpdateGroug group is added, and returns an error message. The following test does not validate constraintViolations2 because the Default group is used. If you want to leave the group unmarked, the Default group will also be verified, and you can inherit the Default group

public interface AddGroup extends Default {}Copy the code

Use the Hibernate Validator in Spring

How do you use a Hibernate Validator in Spring? Or how to use Hibernate Validator in a Web project?

The hibernate validator dependency is added to the Spring-boot-starter -web, which indicates that Spring Boot itself uses the Hibernate Validator framework

Configure the Validator

@Configuration
public class ValidatorConfig {

    /** * configure validators **@return validator
     */
    @Bean
    public Validator validator(a) {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                // Fast failure mode
                .failFast(true)
                // .addProperty( "hibernate.validator.fail_fast", "true" )
                .buildValidatorFactory();
        returnvalidatorFactory.getValidator(); }}Copy the code

You can set fast failure mode to failFast(true) or addProperty(” Hibernate.validator.fail_fast “, “true”). Fast failure mode returns the first invalid parameter during validation. The verification of subsequent parameters is not continued. Otherwise, the system checks all parameters at once and returns all error information that does not meet the requirements

If Spring MVC requires XML configuration, see the configuration below

<mvc:annotation-driven validator="validator"/>

<! -- Validator basic configuration -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
    <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
    <! -- Mapping resource files -->
    <property name="validationMessageSource" ref="messageSource"/>
</bean>

<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource" name="messageSource">
    <! --<property name="basenames"> <list> <value>classpath:messages/messages</value> <value>classpath:messages/ValidationMessages</value> </list> </property>-->
    <property name="useCodeAsDefaultMessage" value="false" />
    <property name="defaultEncoding" value="UTF-8" />
    <property name="cacheSeconds" value="60" />
</bean>
Copy the code

Request parameter bean validation

Bean validation on the interface requires either the @Valid or Spring @validated annotations to precede the parameters, both of which result in the application of standard Bean validation. If the validation fails, a BindException is thrown and a 400 (BAD_REQUEST) response is given. Alternatively, validation Errors can be handled locally within the controller through the Errors or BindingResult arguments. In addition, if the parameter before an @ RequestBody annotations, validation error will be thrown MethodArgumentNotValidException anomalies.

@RestController
public class UserController {

    @PostMapping("/user")
    public R handle(@Valid @RequestBody User user, BindingResult result) {
        // Handle validation errors locally within the controller
        if (result.hasErrors()) {
            result.getAllErrors().forEach(s -> System.out.println(s.getDefaultMessage()));
             return R.fail(result.getAllErrors().get(0).getDefaultMessage());
        }
        // ...
        return R.success();
    }

    @PostMapping("/user2")
    public R handle2(@Valid User user, BindingResult result) {
        // Handle validation errors locally within the controller
        if (result.hasErrors()) {
            result.getAllErrors().forEach(s -> System.out.println(s.getDefaultMessage()));
             return R.fail(result.getAllErrors().get(0).getDefaultMessage());
        }
        // ...
        return R.success();
    }

    / * * * verify by throwing a ` MethodArgumentNotValidException ` * /
    @PostMapping("/user3")
    public R handle3(@Valid @RequestBody User user) {
        // ...
        return R.success();
    }

    /** * failed to throw 'BindException' */
    @PostMapping("/user4")
    public R handle4(@Valid User user) {
        // ...
        returnR.success(); }}Copy the code

With Spring’s BindingResult parameter, we can handle validation errors in the controller, but we usually convert the validation error message to our own return format, so it is not necessary to do this in each method. We can do uniform error handling with exceptions that fail validation

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /** * Hibernate validator data binding exception interception **@paramE Binding verification is abnormal *@returnError message */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(BindException.class)
    public R validateErrorHandler(BindException e) {
        ObjectError error = e.getAllErrors().get(0);
        log.info("Data validation exception: {}", error.getDefaultMessage());
        return R.fail(error.getDefaultMessage());
    }

    /** * Hibernate validator data binding exception interception **@paramE Binding verification is abnormal *@returnError message */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R validateErrorHandler(MethodArgumentNotValidException e) {
        ObjectError error = e.getBindingResult().getAllErrors().get(0);
        log.info("Data validation exception: {}", error.getDefaultMessage());
        returnR.fail(error.getDefaultMessage()); }}Copy the code

Method parameter validation

configuration

Hibernate Validators can validate parameters at the method level, and Spring implements them as well.

We are in the configuration of the Validator, add MethodValidationPostProcessorBean, in the above ValidatorConfig. Add the Java configuration

/** * Set method parameter validator */
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor(a) {
    MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
    // Set validator mode to quick failure return
    postProcessor.setValidator(validator());
    return postProcessor;
}
Copy the code

If it is Spring Mvc, declare the bean information in spring-mVc.xml, otherwise it is invalid in the Controller

<! -- Set method parameter validator -->
<bean id="methodValidationPostProcessor" class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
    <property name="validator" ref="validator"/>
</bean>
Copy the code

use

Configured on MethodValidationPostProcessor above, we can use the method parameters or return values constraint annotations, note that on the classes to use parameter validation must be combined with @ Validated annotations, otherwise is invalid

/** * must add '@Validated` comment * /
@Validated
@RestController
public class UserController {

    @GetMapping("/user")
    public R handle(@Mobile String mobile) {
        // ...
        returnR.success(); }}Copy the code

If verification is not through, throws ConstraintViolationException is unusual, in the same way, we can deal with validation errors in global exception handler, add the code in the GlobalExceptionHandler

/** * Spring validator parameter validation exception interception **@paramE Binding verification is abnormal *@returnError message */
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public R defaultErrorHandler(ConstraintViolationException e) { Set<ConstraintViolation<? >> violations = e.getConstraintViolations(); ConstraintViolation<? > violation = violations.iterator().next(); log.info("Data validation exception: {}", violation.getMessage());
    return R.fail(violation.getMessage());
}
Copy the code

grouping

Spring’s @validate annotation supports group validation

@PostMapping("/user")
public R handle(@Validated(AddGroup.class) @RequestBody User user) {
    // ...
    return R.success();
}
Copy the code