[Spring] use Spring's Validation to complete data backend verification

preface

Data verification is an indispensable function of interactive websites. The front-end js verification can cover most verification responsibilities, such as user name uniqueness, birthday format, mailbox format verification and so on. However, in order to avoid users bypassing the browser and using http tools to directly request some illegal data from the back end, the data verification of the server is also necessary to prevent dirty data from falling into the database. If an illegal mailbox format appears in the database, it will also give the operation and maintenance personnel a headache. In my previous research and development of insurance products, the system has strict requirements for data verification and pursues variability and efficiency. I once used drools as the rule engine and served as the verification function. In general applications, you can use the validation to verify the data.

Briefly describe the relationship between JSR303/JSR-349, hibernate validation and spring validation. JSR303 is a standard. JSR-349 is an upgraded version of JSR-349. Some new features are added. They specify some verification specifications, namely verification annotations, such as @ Null, @ NotNull, @ Pattern. They are located in javax validation. Under the constraints package, only specifications are provided, not implementations. Hibernate validation is the practice of this specification (do not associate hibernate with the database orm framework). It provides corresponding implementations and adds some other verification annotations, such as @ Email, @ Length, @ Range, etc. they are located in the org.hibernate.validator.constraints package. In order to provide convenience to the developer, the universal spring secondary encapsulates hibernate validation and displays the verified validated bean You can use spring validation or hibernate validation. Another feature of spring validation is that it adds automatic verification in the spring MVC module and encapsulates the verification information into specific classes. This undoubtedly facilitates our web development. This paper mainly introduces the mechanism of automatic verification in spring MVC.

Introduce dependency

We use maven to build the springboot application for demo demonstration.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

We only need to introduce the spring boot starter web dependency. If you view its child dependencies, you can find the following dependencies:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

After verifying my previous description, the web module uses hibernate validation, and the databind module also provides the corresponding data binding function.

Build startup class

Without adding other annotations, a typical startup class

@SpringBootApplication
public class ValidateApp {

    public static void main(String[] args) {
        SpringApplication.run(ValidateApp.class, args);
    }
}

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Create an entity class that needs to be verified

public class Foo {

    @NotBlank
    private String name;

    @Min(18)
    private Integer age;

    @Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "Mobile phone number format error")
    @NotBlank(message = "Mobile phone number cannot be empty")
    private String phone;

    @Email(message = "Mailbox format error")
    private String email;

    //... getter setter

}

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

Some commonly used verification annotations are easy to understand. The verification content can be inferred from the annotation name in the field. Each annotation contains a message field, which is used as a prompt message when the verification fails. For special verification annotations, such as Pattern, you can also add regular expressions yourself.

Verify data in @ Controller

Spring MVC provides us with the function of automatically encapsulating form parameters. A typical controller with parameter verification is shown below.

@Controller
public class FooController {

    @RequestMapping("/foo")
    public String foo(@Validated Foo foo <1>, BindingResult bindingResult <2>) {
        if(bindingResult.hasErrors()){
            for (FieldError fieldError : bindingResult.getFieldErrors()) {
                //...
            }
            return "fail";
        }
        return "success";
    }

}

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

Notable points:

<1> The @ validated annotation needs to be added before the parameter foo, indicating that spring needs to verify it, and the verified information will be stored in the subsequent bindingresult. Note that it must be adjacent. If multiple parameters need to be verified, the form can be as follows. foo(@Validated Foo foo, BindingResult fooBindingResult ,@Validated Bar bar, BindingResult barBindingResult); That is, a verification class corresponds to a verification result.

<2> The verification results will be automatically filled in. In the controller, specific operations can be determined according to the business logic, such as jumping to the error page.

The most basic verification is completed. The following is a summary of the verifications provided by the framework:

JSR Verification comments provided:         
@Null   The annotated element must be null    
@NotNull    The annotated element must not be empty 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 and its value must be less than or equal to the specified maximum value    
@DecimalMin(value)  The annotated element must be a number and its value must be greater than or equal to the specified minimum value    
@DecimalMax(value)  The annotated element must be a number and its 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   The 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    


Hibernate Validator Verification comments provided:  
@NotBlank(message =)   Validation string is not null,And the length must be greater than 0    
@Email  The annotated element 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

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

Calibration experiment

We make a test request for the verification entry implemented above:
visit http://localhost:8080/foo?name=xujingfeng&email=000&age=19 You can get the following debug information:

The experiment tells us that the verification results play a role. Moreover, it can be found that when multiple errors occur, spring validation will not stop immediately after the first error occurs, but continue to try and tell us all the errors. debug can view more rich error information. These are convenient features provided by spring validation, which are basically applicable to most scenarios.

You may not be satisfied with the simple verification feature. Here are some additions.

Group check

If the same class has different verification rules in different usage scenarios, group verification can be used. Minors are not allowed to drink, but we do not make special restrictions in other scenarios. How does this demand reflect the same entity and different verification rules?

Rewrite annotation and add group:

Class Foo{

    @Min(value = 18,groups = {Adult.class})
    private Integer age;

    public interface Adult{}

    public interface Minor{}
}

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

This shows that the 18-year-old restriction works only under the Adult group.

Controller layer override:

@RequestMapping("/drink")
public String drink(@Validated({Foo.Adult.class}) Foo foo, BindingResult bindingResult) {
    if(bindingResult.hasErrors()){
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            //...
        }
        return "fail";
    }
    return "success";
}

@RequestMapping("/live")
public String live(@Validated Foo foo, BindingResult bindingResult) {
    if(bindingResult.hasErrors()){
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            //...
        }
        return "fail";
    }
    return "success";
}

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

The drink method requires Adult verification, while the live method does not.

Custom verification

Business requirements are always more complex than these simple checks provided by the framework. We can customize the checks to meet our needs. Customizing spring validation is very simple. It is mainly divided into two steps.

1. User defined verification annotation
We tried to add a "string cannot contain spaces" restriction.

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CannotHaveBlankValidator.class})<1>
public @interface CannotHaveBlank {

    //Default error message
    String message() default "Cannot contain spaces";

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

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

    //Use when specifying multiple
    @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        CannotHaveBlank[] value();
    }

}

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

We don't need to pay much attention. The principle of using spring validation is to facilitate our development, such as payload, List and groups, which can be ignored.

<1> The custom annotation specifies the real verifier class for this annotation.

2 write the real verifier class

public class CannotHaveBlankValidator implements <1> ConstraintValidator<CannotHaveBlank, String> {

    @Override
    public void initialize(CannotHaveBlank constraintAnnotation) {
    }


    @Override
    public boolean isValid(String value, ConstraintValidatorContext context <2>) {
        //Do not check when null
        if (value != null && value.contains(" ")) {
            <3>
            //Get default prompt information
            String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
            System.out.println("default message :" + defaultConstraintMessageTemplate);
            //Disable default prompt
            context.disableDefaultConstraintViolation();
            //Set prompt
            context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation();
            return false;
        }
        return true;
    }
}

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

<1> All verifiers need to implement the ConstraintValidator interface, which is also very visual, including an initialization event method and a method to judge whether it is legal or not.

public interface ConstraintValidator<A extends Annotation, T> {

    void initialize(A constraintAnnotation);

    boolean isValid(T value, ConstraintValidatorContext context);
}

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

<2> The constraintvalidatorcontext context contains all the information in the authentication. We can use this context to obtain the default error prompt information, disable the error prompt information, rewrite the error prompt information, etc.

<3> Some typical verification operations may enlighten you.

It is worth noting that custom annotations can be used in method, field and annotation_ Type, constructor and parameter, the second generic parameter T of ConstraintValidator is the type to be verified.

Manual verification

In some scenarios, we may need to perform manual verification, that is, use the verifier to validate the entities to be verified and obtain the verification results synchronously. Theoretically, we can use Hibernate Validation to provide validators or spring to encapsulate them. In spring built projects, it is recommended to use spring encapsulated methods. Here are two methods:

Hibernate Validation:

Foo foo = new Foo();
foo.setAge(22);
foo.setEmail("000");
ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
Validator validator = vf.getValidator();
Set<ConstraintViolation<Foo>> set = validator.validate(foo);
for (ConstraintViolation<Foo> constraintViolation : set) {
    System.out.println(constraintViolation.getMessage());
}

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Because we rely on the Hibernate Validation framework, we need to call Hibernate related factory methods to obtain the validator instance to verify.

In the Validation section of the spring framework document, you can see the following description:

Spring provides full support for the Bean Validation API. This includes convenient support for bootstrapping a JSR-303/JSR-349 Bean Validation provider as a Spring bean. This allows for a javax.validation.ValidatorFactory or javax.validation.Validator to be injected wherever validation is needed in your application. Use the LocalValidatorFactoryBean to configure a default Validator as a Spring bean:

bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"

The basic configuration above will trigger Bean Validation to initialize using its default bootstrap mechanism. A JSR-303/JSR-349 provider, such as Hibernate Validator, is expected to be present in the classpath and will be detected automatically.

The above paragraph mainly describes that spring fully supports JSR-303 and JSR-349 standards for validation, and encapsulates the implementation of LocalValidatorFactoryBean as validator. It is worth mentioning that the responsibility of this class is actually very important. It is compatible with spring's validation system and Hibernate's validation system. It can also be called directly by developers to replace the hibernate validator obtained from the factory method above. Because we use springboot, it will trigger the automatic configuration of the web module. The localvalidator factorybean has become the default implementation of the validator, which only needs to be injected automatically.

@Autowired
Validator globalValidator; <1>

@RequestMapping("/validate")
public String validate() {
    Foo foo = new Foo();
    foo.setAge(22);
    foo.setEmail("000");

    Set<ConstraintViolation<Foo>> set = globalValidator.validate(foo);<2>
    for (ConstraintViolation<Foo> constraintViolation : set) {
        System.out.println(constraintViolation.getMessage());
    }

    return "success";
}

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

<1> Readers who have really used the Validator interface will find that there are two interfaces, one is located in javax Under the validation package, the other is located at org springframework. Under the validation package, note that we use the former javax Validation, which is spring's own built-in verification interface. LocalValidatorFactoryBean implements both interfaces.

<2> The final implementation class of the verification interface here is LocalValidatorFactoryBean.

Method based verification

@RestController
@Validated <1>
public class BarController {

    @RequestMapping("/bar")
    public @NotBlank <2> String bar(@Min(18) Integer age <3>) {
        System.out.println("age : " + age);
        return "";
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public Map handleConstraintViolationException(ConstraintViolationException cve){
        Set<ConstraintViolation<?>> cves = cve.getConstraintViolations();<4>
        for (ConstraintViolation<?> constraintViolation : cves) {
            System.out.println(constraintViolation.getMessage());
        }
        Map map = new HashMap();
        map.put("errorCode",500);
        return map;
    }

}

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

<1> Add @ Validated annotation to class

<2> < 3 > return value and input parameter of verification method

<4> Add an exception handler to get the information about the attributes that fail to pass the verification

Based on the verification of the method, I don't recommend it personally. I don't think it is well combined with the project.

Some ideas of using validation framework

Theoretically, spring validation can implement many complex verifications. You can even make your Validator obtain ApplicationContext, obtain all resources in the spring container, perform database verification, and inject other verification tools, Complete combination verification (e.g. consistent passwords) and so on, but finding a balance between ease of use and encapsulation complexity is what we, as tool users, should consider. My preferred method is to complete some simple and reusable verifications only using the built-in annotations and user-defined annotations. For complex verifications, they are included in the business code, such as user names after all Whether there is such a check, it is not enough to rely only on database query. In order to avoid concurrency problems, additional work such as unique index must be added, isn't it?

Keywords: Back-end

Added by RootKit on Tue, 28 Dec 2021 04:46:31 +0200