“This is the 19th day of my participation in the First Challenge 2022, for more details: First Challenge 2022”.

Based on the latest Spring 5.x, this paper introduces Spring’s annotation-based declarative data verification in detail, including the automated data verification method based on javax.Validation. Valid annotation and Spring’s @Validated annotation.

Web project, the back-end to the front-end calibrated parameters passed is always inevitable, such as length, field size, null check, etc., although some check front will do, but in order to increase the robustness and security of the web application (for example, if the bypass front-end sent request directly, parameters can not be guaranteed at this time). For important interfaces, it is necessary for the back end to perform parameter verification twice!

Past web project, the parameter calibration, either through a series of handwritten if statements in the controller method to judgment, either to extract a common calibration tools, and then manually validated methods to call tools, for large projects, no matter use which kinds of means, still have a lot of work!

After using the Spring MVC framework, after the data binding of the request is successful, you can base onJavax.mail. Validation. Valid annotationorThe @Validated annotation of SpringAutomates data validation and supports configuration of validators for both global and individual requests. It is very convenient and simple to use, greatly reducing the burden of developers!

Spring MVC learning series

Spring MVC Learning (1) — An introduction to MVC and an introduction to Spring MVC

Spring MVC Learning (2) – The container hierarchy in Spring MVC and the concept of parent-child containers

Spring MVC Learning (3) – The core components in Spring MVC and the execution flow of the request

Spring MVC Learning (4) — A detailed introduction and use case of the ViewSolvsolver view parser

Spring MVC Learning (5) – Annotation-based Controller configuration complete solution

Spring MVC Learning (6) – Spring data type conversion mechanism

Annotation-based declarative Validation of data

Spring MVC Learning (8) – HandlerInterceptor processor interceptor mechanism complete solution

Spring MVC Learning (9) – Project unified exception handling mechanism details and use cases

Spring MVC Learning (10) – File upload configuration, path configuration of DispatcherServlet, request and response content encoding

Spring MVC Learning (11) – Introduction to cross-domain and solving cross-domain problems using CORS

1 Bean Validation

2.0 Java Bean ValidationLater renamedJakarta Bean ValidationNew versions of the app from 2018 onwards all start with Jakarta, according to the official websitehttps://beanvalidation.org/For now, the maven dependency source code is basically the same, the biggest difference is the name of the latest dependencyjakarta.validation:jakarta.validation-apiAnd the latest version relies on the previous versionjavax.validationOld version dependencies are no longer updated!

Jakarta Bean Validation provides oneJava BeanValidation specification, also known asJSR-303The specification, which proposes a declarative Bean validation constraint capability, can be bound to model properties using the annotation help in the specification, and these constraints can then be enforced by the runtime, as well as custom constraint annotations, as shown in the following example:

public class PersonForm {

    @NotNull
    @Size(max=64)
    private String name;

    @Min(0)
    private int age;
}
Copy the code

Jakarta Bean ValidationIt is just a set of specifications and does not provide any implementations, but so far some vendors have provided their own implementations and we can use them by importing their Maven coordinates, the most common beinghibernate-validator.Hibernate - Validator and Hibernate ORM frameworkThere’s no connection. The only connection is that they’re both made ofHibernate teamThe official website is:https://hibernate.org/validator/.

1.1 Built-in constraint annotations

hibernate-validatorprovidesJakarta Bean ValidationFull implementation of the built-in constraint annotations, while you can customize the constraints! At the same time,hibernate-validatorrightJakarta Bean ValidationThe use of built-in constraint annotations has been extended, for example@MaxApplies to strings. See also:https://docs.jboss.org/hibernate/validator/7.0/reference/en-US/html_single/#section-builtin-constraints.

The built-in constraints are locatedjakarta.validation.constraintsIn the package! All annotations are availableMessage, groups, payloadProperties:

  1. message: Returns the error message created when a constraint is violated. You can use {key} to set the default key, and the default value will be automatically searched. If no value exists, the default error message will be the value of the key. The element value of the annotation can be obtained in the message with {elementName}, the value of the annotation annotation can be obtained with ${validatedValue}, or other EL expressions can be used.
  2. payload: You can assign a user-defined valid Payload object to a constraint. It is usually not used.

In addition, they have the following features:

annotations describe Supported types (runtime throws UnexpectedTypeException if these annotations are on a type they do not support)
@AssertFalse Check if the value of the annotation is false. A null value is considered valid. Boolean and Boolean types are supported.
@AssertTrue Check if the value of the annotation is true. A null value is considered valid.
@DecimalMax(value=, inclusive=) When inclusive=false, check whether the value of the annotation is less than the specified maximum, and otherwise, check whether the value is less than or equal to the specified maximum. The value argument is internally compared with the BigDecimal representation converted to by new BigDecimal(value). A null value is considered valid. Supports validation of BigDecimal, BigInteger, CharSequence, Byte, short, int, long, and their wrapper types. Note that double and float are not supported due to rounding errors.

The realization of the hv also support Number and its subtypes, and javax.mail. Money. MonetaryAmount type dependent JSR 354 (if present). Double and float are also supported, but comparisons can be inaccurate.
@DecimalMin(value=, inclusive=) When inclusive=false, check whether the value of the annotation is greater than the specified minimum, otherwise, check whether the value is greater than or equal to the specified minimum. The value argument is internally compared with the BigDecimal representation converted to by new BigDecimal(value). A null value is considered valid.
@Digits(integer=, fraction=) Check if the value of the annotation is a number in the range of the most integer and fraction bits. A null value is considered valid.
@Max(value=) Checks that the value of the annotation is less than or equal to the maximum specified. A null value is considered valid. Supports validation of BigDecimal, BigInteger, Byte, short, int, long, and their wrapper types. Note that double and float are not supported due to rounding errors.

In addition, the implementation of hv also supports CharSequence and its subtypes (by the numerical calculation of the character sequence said), as well as the Number and its subtype and javax.mail. Money. MonetaryAmount type dependent JSR 354 (if present). Double and float are also supported, but comparisons can be inaccurate.
@Min(value=) Checks that the value of the annotation is less than or equal to the maximum specified. A null value is considered valid.
@Email Check that the value of the annotation is a valid E-mail address. The optional arguments regexp and flags allow you to specify additional regular expressions (including regular expression flags) that the email must match. Supports CharSequence and its subtypes.
@Future Check if the value of the annotation is a future time. A null value is considered valid. Java.util. Date, java.util.Calendar, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime, java.time.MonthDay, java.time.OffsetDateTime, java.time.OffsetTime, java.time.Year, java.time.YearMonth, java.time.ZonedDateTime, java.time.chrono.HijrahDate, java.time.chrono.JapaneseDate, Java. Time. Chrono MinguoDate, Java. Time. The chrono. ThaiBuddhistDate type.

In addition, the IMPLEMENTATION of HV supports the Joda Time date/ Time type if there is a Joda Time dependency, as well as any implementation that supports ReadablePartial and ReadableInstant.
@FutureOrPresent Check whether the value of the annotation is the current time or the future time. A null value is considered valid.
@Past Check if the value of the annotation is a past time. A null value is considered valid.
@PastOrPresent Check whether the value of the annotation is the current time or the past time.
@NotBlank Checks that the value of the annotation is not NULL and must contain at least one non-whitespace character. Supports CharSequence and its subtypes.
@NotEmpty Check that the value of the annotation is not null and is not null. Support CharSequence and its subtypes (calculate the length of character sequences, that is, cannot be “”), Collection, Map, and Array (contain at least one element/key-value pair).
@NotNull Check if the value of the annotation is not NULL. Any type is supported.
@Null Check if the value of the annotation is NULL.
@Negative Check if the value of the annotation is strictly negative (0 does not pass). A null value is considered valid. Supports BigDecimal, BigInteger, byte, short, int, long, float, double, and their wrapper types.

In addition, the implementation of hv also supports CharSequence and its subtypes (by the numerical calculation of the character sequence said), as well as the Number and its subtype and javax.mail. Money. MonetaryAmount type dependent JSR 354 (if present).
@NegativeOrZero Check if the value of the annotation is negative or 0. A null value is considered valid. A null value is considered valid.
@Positive Check that the value of the annotation is strictly positive (0 does not pass). A null value is considered valid.
@PositiveOrZero Check if the value of the annotation is positive or 0. A null value is considered valid.
@Pattern(regex=, flags=) To check whether the value of the annotation matches the given regular expression (regex), consider that the null value of the given token match (FALgs) is considered passed. Supports CharSequence and its subtypes.
@Positive Check if the value of the annotation is strictly positive (0.0 passes, other zeros fail). A null value is considered valid. Supports BigDecimal, BigInteger, byte, short, int, long, float, double, and their wrapper types.

In addition, the implementation of hv also supports CharSequence and its subtypes (by the numerical calculation of the character sequence said), as well as the Number and its subtype and javax.mail. Money. MonetaryAmount type dependent JSR 354 (if present).
@PositiveOrZero Check if the value of the annotation is positive or 0. A null value is considered valid.
@Size(min=, max=) Check that the value of the annotation is between the given minimum value (default 0) and maximum value (default integer.max_value), containing both endpoint values. A null value is considered valid. Supports CharSequence and its subtypes (counting the length of character sequences), Collection, Map, and Array (counting the number of elements/key-value pairs).

1.2 Notes on other constraints

In addition to theJakarta Bean Validation APIIn addition to the constraint annotations of the definition,Hibernate ValidatorThere are also several useful custom constraint annotations that you may not need, but you can look at in case you need them!

annotations describe Supported types
@CreditCardNumber(ignoreNonDigitCharacters=) Check that the value of the annotation passes the Luhn checksum test, that is, the format of the U.S. credit card number is correct. IgnoreNonDigitCharacters Specifies whether non-numeric characters are allowed to be ignored. The default value is false. A null value is considered valid. Supports CharSequence and its subtypes.
@Currency(value=) Check annotation javax.mail. Money. Whether the monetary unit of MonetaryAmount belongs to include unit at the specified value in the array. A null value is considered valid. Support javax.mail. Money. MonetaryAmount subtype and its dependence on JSR 354 (if present).
@DurationMax(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=) Check that the time difference represented by the annotation’s java.time.Duration is not greater than the value specified by the annotation, and equality is allowed if the inclusive flag is set to true. A null value is considered valid. The java.time.Duration type is supported.
@DurationMin(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=) Check that the time difference represented by the annotation’s java.time.Duration is not less than the value specified by the annotation, and equality is allowed if the inclusive flag is set to true. A null value is considered valid.
@EAN Check that the value of the annotation is a valid EAN (commodity barcode). The type attribute specifies the EAN type. The default is EAN-13. A null value is considered valid. Supports CharSequence and its subtypes.
@ISBN Check that the value of the annotation is a valid ISBN. The type attribute specifies the ISBN type. The default is ISBN-13. A null value is considered valid. Supports CharSequence and its subtypes.
@Length(min=, max=) Check that the length of the value of the annotation is between the minimum and maximum values, including both endpoint values. A null value is considered valid. Supports CharSequence and its subtypes.
@CodePointLength(min=, max=, normalizationStrategy=) Check that the code point length of the value of the annotation is between the minimum and maximum values, including both endpoint values. If normalizationStrategy is set, the normalized values are validated. A null value is considered valid. Supports CharSequence and its subtypes.
@Range(min=, max=) Checks if the value of the annotation is between the specified minimum and maximum, containing both endpoint values. A null value is considered valid. Supports BigDecimal, BigInteger, byte, short, int, long, float, double, and their wrapper types.
@UniqueElements Check to see if the collection of annotations contains only unique elements, using the equals() method to compare elements for equality. A null value is considered valid. Collection and its subtypes are supported.
@URL(protocol=, host=, port=, regexp=, flags=) Check to see if the value of the annotation is a valid URL according to the RFC2396 specification. If any optional parameters are specified in the annotation: protocol, host, or port, the corresponding URL fragment must match the specified value. The optional arguments regexp and flags allow you to specify that the URL must match additional regular expressions (including regular expression flags). By default, this constraint uses the constructor of java.net.URL to verify that a given string represents a valid URL. Supports CharSequence and its subtypes.

1.3 to integrate hibernate validator

Spring support for easy integration with Hibernate-Validator is at the heart of performing Bean validationsjavax.validation.ValidatorThe Validator needs to be introducedBean Validation providerThat is, the implementation of a concrete Bean Validation, such as hibernate-Validator. At run time, the Validator relies on the implementation of the concrete Bean Validation for Validation.

For ordinary projects, we need to introducehibernate-validatorOr other related implementation dependencies that introduce Bean validation, forSpring BootThe project automatically introduces dependencies, we don’t need to!

<! -- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate- validator -->
<! Hibernate ->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.18. The Final</version>
</dependency>
<! -- https://mvnrepository.com/artifact/javax.el/javax.el-api -->
<! Tomcat 7 May require this dependency -->
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.el</artifactId>
    <version>3.0.1 - b08</version>
</dependency>
Copy the code

Note that the Bean Validation API for the hibernate- Validator 7 version is provided fromjavax.validationReplaced with ajakarta.validation, so if you switch versions, you may need to re-import the API’s classpath.

1.4 Configuring the Validator Bean

If we want to configure manually invoked the Validator in other places, will be provided by the Spring to LocalValidatorFactoryBean to quickly configure the Validator for management of Spring beans!

If need to manually check, because LocalValidatorFactoryBean implements javax.mail. Validation. ValidatorFactory and javax.mail. Validation. The Validator, And Spring org. Springframework. Validation. The Validator, so we can be of any interface reference injection need to manually call the bean validation logic.

@Bean
public LocalValidatorFactoryBean validator(a) {
    LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
    Spring automatically looks up the implementation of the Provider even if no Provider is set
    //localValidatorFactoryBean.setProviderClass(HibernateValidator.class);
    return localValidatorFactoryBean;
}
Copy the code

2 Spring MVC parameter attribute verification

This configuration only validates the properties of the parameter object bound to the controller method of Spring MVC. If you need to validate the parameter itself and the return value, you will need to be introduced later!

The validation is done after the data binding is successful. Similar to data binding, Spring MVC binding parameter property validation supports both global and local configuration.

2.1 Global and Local Configurations

For Spring MVC parameter binding verification in normal projects, we only need to pass@EnableWebMvcor<mvc:annotation-driven/>Open the MVC configuration and Spring MVC will automatically try to find what is introduced in the classpathValidationProviderThe first Provider found is configured as a global Validator. This eliminates the need for manual configuration. If you just introducehibernate-validatorThen there is only one Provider implementation —HibernateValidator. For Spring Boot projects, MVC configuration is automatically enabled, again without us having to manually enable it.

If we needed to manually specify the global Validator to use for Spring MVC, the configuration would be as follows:

/ * * *@author lx
 */
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    If null is returned, and if the implementation of jSR-303 Provider exists in the classpath, Will be created by default types for * org. Springframework. Validation. Beanvalidation. OptionalValidatorFactoryBean Validator * /
    @Override
    public Validator getValidator(a) {
        LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
        Spring automatically looks up the implementation of the Provider in the classpath even if no Provider is set
        //localValidatorFactoryBean.setProviderClass(HibernateValidator.class);
        returnlocalValidatorFactoryBean; }}Copy the code

The following example shows how to implement the same configuration in XML:

<! -- The validator attribute is used to specify the Validator bean. This attribute is not required unless you want to customize the validator.
<! -- If not specified, the jSR-303 Provider implementation is automatically looked up in the classpath to configure a provider of type -->
<! -- org. Springframework. Validation. Beanvalidation. OptionalValidatorFactoryBean Validator - >
<mvc:annotation-driven validator="mvcValidator"/>
Copy the code

Of course, we can also register a local Validator for some requests:

/** * Add a local Validator */
@InitBinder
public void initBinder(WebDataBinder dataBinder) {
    // If you are using a predefined Validator, you need to manually initialize afterPropertiesSet
    // If you are using a custom Validator implementation, the SUPPORTS method is required to support the parameter type to be validated otherwise an exception will be thrown
    LocalValidatorFactoryBean optionalValidatorFactoryBean = new LocalValidatorFactoryBean();
    optionalValidatorFactoryBean.afterPropertiesSet();
    // Multiple validators can be added
    dataBinder.addValidators(optionalValidatorFactoryBean);
}
Copy the code

If global validators and local validators are added, a chain of validators will be formed during validation. The global validators will be called first, and each Validator will be called in order of validation.

2.2 Test Cases

After enabling MVC configuration, Spring MVC’s global Validator will be automatically configured. Let’s test it out! We use lmBOK’s @data annotation to omit setters, getters, and toString methods. If not, write them by hand.

Select * from entity where id, sex, and name cannot be null, id must be positive, sex must be 0 or 1, and name must be between 1 and a character:

@Data
public class User1 {

    @Positive
    @NotNull
    private Long id;

    @Range(min = 0, max = 1)
    @NotNull
    private Byte sex;

    @Size(min = 1, max = 10)
    @NotBlank
    private String name;
}
Copy the code

The next step is the controller method, and a very important step is that we need to prefix the object parameter that validates the object properties@Validated or @Valid annotation.javax.validation.ValidAnnotations can be marked on properties, method parameters, or method return types to indicate cascading validation of the marked object, that is, the properties inside the object, andorg.springframework.validation.annotation.ValidatedThe Spring annotation provides the same functionality as @valid, but also adds the ability to select validation groups, which we’ll see later!

/ * * *@author lx
 */
@RestController
public class GlobalMvcValidationController {

    /** * Support application/json request parameters validation */
    @PostMapping("/pv1")
    @ResponseBody
    public User1 pv1(@Validated @RequestBody User1 user1) {
        System.out.println(user1);
        user1.setId(0L);
        return user1;
    }

    /** * support common request parameter verification */
    @GetMapping("/pv2")
    @ResponseBody
    public void pv2(@Valid User1 user1) { System.out.println(user1); }}Copy the code

Access/pv3? Id =1&sex=2&name=name;

The console can see more explicit exception information (we’ll see how to integrate exceptions thrown by data validation into global exception handling later) :

If access /pv3? Id =-1&sex=2&name=name;

Continue with/PV3? Id =1&sex=1&name=name

2.3 Cascading Verification

Validation Engine automatically validates the @VALID attribute if the validation object contains the @VALID attribute. Similarly, cascaded checkmated objects that are null are ignored (considered checkmated).

The following entity has an internal object attribute. We use the @valid annotation for cascading verification:

@Data
public class User2 {

    @Positive
    @NotNull
    private Long id;

    @Range(min = 0, max = 1)
    @NotNull
    private Byte sex;

    @Size(min = 1, max = 10)
    @NotBlank
    private String name;

    / * * *@ValidAnnotations to cascade validation of properties of object types */
    @Valid
    @NotNull
    private Address address;

    @Data
    public class Address {
        @NotBlank
        @Pattern(regexp = "\\d{6}")
        private String postcode;
        @NotBlank
        @Size(min = 10, max = 100)
        private String workAddress;
        @NotBlank
        @Size(min = 10, max = 100)
        privateString homeAddress; }}Copy the code

Controller method:

/** * Cascading verification */
@PostMapping("/pv3")
@ResponseBody
public User2 pv3(@RequestBody @Valid User2 user2) {
    System.out.println(user2);
    return user2;
}
Copy the code

Test data:

{
  "id": 1."sex": 1."name": "name"."address": {
    "postcode": "011111"."workAddress": "1111111111"."homeAddress": "1111111111"}}Copy the code

The result, of course, is a pass:

Change the postcode attribute of the internal object to 5 characters to test again:

{
  "id": 1."sex": 1."name": "name"."address": {
    "postcode": "11111"."workAddress": "1111111111"."homeAddress": "1111111111"}}Copy the code

The cascading verification fails because postcode does not conform to the specified schema:

2.4 Container Verification

The Java container includesCollection, Map, and ArrayAnd other common types.

@ValidInternal validation of container elements is also supported. Container attributes of any type can be annotated with the @VALID annotation. This will result in a cascade validation of each element (map includes key and value). buthibernate validatorIt is not recommended to do this, but rather to add to the specific generic type of the container, which has the advantage of being more specific for validation purposes. There seem to be some limitations on array validation, such as the fact that null elements are not currently validated by tests.

When verifying container elements, any element that does not meet the criteria is considered to have failed the verification.

The following entities:

@Data
public class User3 {

    /** * list contains at least two elements * elements cannot be null and are cascaded */
    @Size(min = 2)
    @NotNull
    @Valid
    private List<@NotNull InnerClass> user1s;

    /** * Map cannot be empty * The key must contain at least two characters but not blank characters, and the value cannot be null and cascading verification is performed */
    @NotEmpty
    private Map<@NotBlank @Size(min = 2) String, @NotNull @Valid InnerClass> stringUser1Map;

    /** * Arrays cannot be empty, and elements are cascaded * Validation seems to have limitations, such as the current test failing@NotNullVerify the null element */
    @NotEmpty
    @Valid
    @NotNull
    private @NotNull InnerClass[] user2s;

    @Data
    public static class InnerClass {
        @NotNull
        @Min(1)
        private Long id;

        @NotBlank
        @Size(min = 5)
        privateString name; }}Copy the code

A controller method:

/** * container element validation */
@PostMapping("/pv4")
@ResponseBody
public User3 pv4(@RequestBody @Valid User3 user3) {
    System.out.println(user3);
    return user3;
}
Copy the code

Test data:

{
	"user1s": [{
		"id": 1."name": "11111"
	}, null]."user2s": [{
		"id": 1."name": "11111"
	}, null]."stringUser1Map": {
		"11": {
			"id": 1."name": "11111"
		},
		"1": {
			"id": 1."name": "11111"}}}Copy the code

3. Verification of Spring driver method

3.1 Limitations of Spring MVC parameter attribute verification

In the above”Spring MVC parameter attribute verificationThe default validation of the MVC configuration is only for the properties of the parameter object bound to the Controller method of Spring MVC, if necessaryMethod parameters themselvesorMethod return valueFor example, to verify that the method parameters and return values themselves are not NULL, or to perform the same Bean Validation for methods that are not part of a controller, we need to configure Spring-driven method validation.Spring driver method verificationThe validation rules for the Spring MVC parameter properties are the same as those for the Spring MVC parameter properties above, except that it is more widely used, and spring-driven method validation should be configured manually in normal projects. Similarly, if it’s a Spring Boot Web project, Spring Boot is automatically configured for us, and we don’t need any configuration!

Next, let’s test the limitations of the default Spring MVC parameter attribute validation!

The following GlobalValidationController controller class, the former two methods needs to receive the String, the parameters of the Long, actually for this kind of custom types of parameters, we can only write check’s annotation on the parameters of the controller method, The last two methods are our custom User1 parameters, but the third method wants to check that the passed User1 object is not null, and the fourth method wants to check that the set type parameter has at least 2 elements, etc. In this case, Validation annotations can also only be written to controller method parameters!

/ * * *@author lx
 */
@RestController
public class GlobalValidationController {

    /** * the STR argument cannot be an empty string of 2 to 5 characters */
    @GetMapping("/pv5")
    @ResponseBody
    public String pv5(@Valid @Size(min = 2, max = 5) @NotBlank String str) {
        System.out.println(str);
        return str;
    }

    /** * the id parameter cannot be null and the minimum value is 333 */
    @GetMapping("/pv6/{id}")
    @ResponseBody
    public Long pv6(@PathVariable @Valid @Min(333) @NotNull Long id) {
        System.out.println(id);
        return id;
    }

    /** * the User1 argument itself cannot be null */
    @PostMapping("/pv7")
    @ResponseBody
    public User1 pv7(@RequestBody(required = false) @Valid @NotNull User1 user1) {
        System.out.println(user1);
        return user1;
    }

    /** * the List
      
        argument itself cannot be null and contains at least two elements, and the internal element cannot be NULL */
      
    @PostMapping("/pv8")
    @ResponseBody
    public List<User1> pv7(@RequestBody @Valid @Size(min = 2) List<@NotNull User1> user1List) {
        System.out.println(user1List);
        returnuser1List; }}Copy the code

With the configuration of Spring MVC parameter property verification, we access the interface!

First visit /pv5? STR =2, which clearly does not meet the verification requirements, but returns normal:

/pv6/111 /pv6/111 /pv6/111 /pv6/111 /pv6/111

Then call /pv7 and pass no data, the argument will be null, resulting in invalid verification, but still return normal:

We then call /pv8 and pass no data, passing an empty set of arguments, which obviously does not meet the verification requirements, but still returns normal:

3.2 Verifying the configuration and principle of Spring driver method

To verify the applicability of the more widely, we need to configure the Spring driven method validation, configuration method is very simple, only need to configure a MethodValidationPostProcessor bean in the Spring container to:

/ * * *@author lx
 */
@Configuration
public class SpringConfig {

    @Bean
    public MethodValidationPostProcessor validationPostProcessor(a) {
        return newMethodValidationPostProcessor(); }}Copy the code

If you start the MVC configuration and have Spring MVC parameter attribute validation enabled, then the cascading validation of the controller methods will be done using the validators in the MVC configuration!

MethodValidationPostProcessor is a handy BeanPostProcessor implementations, it is entrusted to the JSR – 303 the Provider of the annotated method execution method level check. Supports validation of method parameters, attributes inside parameters, and method return values (annotations can be annotated on methods), such as:

public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)
Copy the code

The target class/interface to which the method to validate belongs should be marked Spring@ValidatedAnnotation (specify@ValidInvalid) so that Spring can search its internal methods for method validation. You can also specify the @ validation group. By default,JSR-303Validates only against its default group.

The principle of MethodValidationPostProcessorQuite simply, one is created internally when it initializes itselfValidatorAnd a Advisor notifier that contains the Validator, as it acts as aBeanPostProcessor, so inpostProcessAfterInitialization()Method to determine each spring-managed bean instance and pass internal values for qualified beansAdvisorThe notifier, which then creates the proxy bean object and returns it, essentially injects an internal Advisor notifier that will be called when the proxy object’s method is calledMethodValidationInterceptorInterceptors intercept and exploit target methodsValidatorVerify the method.

In other words, it will passSpring AOPThe mechanism creates proxy objects by adding verification logic before and after the target method is executed to implement parameter and return value verification, so it does not matter whether the controller class, as long as the Bean is managed by Spring has inherited into the verification object!

However, since it is based onSpring AOPControl parameter validation, then Spring AOP’s restrictions apply here as well:

  1. AOP’s mechanisms lead to validation in the same AOP classMethods call each otherMethod is invoked, when the check will not take effect, because of the Spring AOP proxy mechanism eventually by the original target object itself to invoke the target method, which could be because the called method is original object calls without being intercepted, of course, there are solutions, that is to get a proxy object, through a proxy object to call the inner method!
  2. Whether based on JDK dynamic proxies or CGLIB proxiesBecause of their own shortcomings, they have limitations on the enhancement of the methods they proxy. For JDK proxies, target classes must implement compliant interfaces and only proxy methods. For CGLIB proxies, target classes cannot be final. And methods that require proxies cannot be private/final/static. The limitations of these AOP proxies are also the limitations of the validation enhancement methods. The actual proxy method is JDK proxy first, then try CGLIB proxy, which we covered in the Spring AOP section, and currently cannot be specified manually.
  3. and@AsyncAs with comments, Spring cannot be@ Validated annotationsAnnotation classes that address setter methods and reflection field annotationsCircular dependency injection (including self-injection), will throw: “… This means that said other beans do not use the final version of the bean…” Exception, simply because the AOP proxy objects instead of using general AbstractAutoProxyCreator method to create, but after using MethodValidationPostProcessor processors to create, the Spring is no solution to this problem. The solution is to add an @lazy annotation to the imported dependency by adding another layer of AOP proxies… . In others, Spring can resolve cyclic dependencies such as AOP proxies created by transactions or custom AOP mechanisms.

3.3 Test Cases

The validation is the same, but it’s important to note that we use it on classes that require spring-driven validationSpring's @ ValidatedNotes, like thisMethodValidationPostProcessorThis class of beans will be enhanced before!

We configure a MethodValidationPostProcessor:

/ * * *@author lx
 */
@Configuration
public class SpringConfig {

    @Bean
    public MethodValidationPostProcessor validationPostProcessor(a) {
        return newMethodValidationPostProcessor(); }}Copy the code

Then, add @ Validated on just GlobalValidationController class:

Visit /pv5 again? STR =2, and something magical happens: our validation annotation works:

Access/pv5? STR =12, then the response is normal:

Then go to /pv6/111 and our validation annotations are in effect as well:

Then call /pv7 and pass no data, the argument will be null, and our validation annotation will also work:

Pass an invalid JSON string:

{
  "id": 10."sex": 2."name": "1"
}
Copy the code

The result will also be verified:

Then we call /pv8 and pass an empty set, and our validation annotation also works:

If two null arguments are passed, the @notnull annotation is also valid:

3.4 Return value verification

Validation annotations for return values can be marked on methods or before the type of the return value! Note that even if the return check fails, the business code must have been executed!

1. The return value cannot be null */
@GetMapping("/pv9")
@ResponseBody
public @NotNull Date pv7(a) {
    System.out.println("----- Business logic -----");
    return null;
}
Copy the code

Visit /pv9 and the result is as follows:

3.5 Non-controller Verification

Because spring-driven method validation is based on Spring AOP, even non-controller classes such as beans in the Service layer support method validation!

Although the Controller layer is typically designed without interfaces, if other layers of a Web project (such as the Service layer) have an interface-implementation class design approach, the following considerations apply:

  1. Parameters on the constraints of the annotation should be defined on the interface be rewritten/superclass method, or even rewrite the presence of constraints on the method of annotation should also and the interface/constraint annotations on the superclass method completely consistent, otherwise the runtime will throw ConstraintDeclarationException exception!
  2. The @ age tag can be used on either the implementation class or the interface/parent class. Annotation on interface/superclass indicates that method validation is enabled for any implementation class or subclass. Annotation on an implementation class indicates that method validation is enabled only for that class.
  3. The return value of the constraint annotation, annotation on the implementation class or interface/parent class method, if annotated (unless the annotation is identical), then all annotations will be validated.

The following Service:

public interface OtherValidationService {

    void parameterValidation(@Valid @NotNull User1 user1);

    @Size(min = 5) String returnValidation(a);
}




@Service
@Validated
public class OtherValidationServiceImpl implements OtherValidationService {
    
    @Override
    public void parameterValidation(User1 user1) {
        System.out.println(user1);
    }

    @Override
    public @NotBlank @Size(min = 4) String returnValidation(a) {
        return ""; }}Copy the code

Controller class:

@RestController
@Validated
public class OtherValidationController {

    @Resource
    private OtherValidationService otherValidationService;

    @GetMapping("/parameterValidation")
    public void user1(a) {
        otherValidationService.parameterValidation(null);
    }

    @GetMapping("/returnValidation")
    public String returnTest(a) {
        returnotherValidationService.returnValidation(); }}Copy the code

Visit /parameterValidation and the result is as follows:

Visit /returnValidation and the result is as follows:

4 Configure the user-defined constraints

The built-in constraint annotations already cover almost all validation needs, but there is always a special need to define your own validation rules. To create a custom constraint, it is easy to perform the following three steps:

  1. Create a custom Constraint annotation using @constraint as the meta-annotation.
  2. Create a validator validator, usually javax.mail. Validation. ConstraintValidator interface, and associated with @ the Constraint;
  3. Define default error messages;

Let’s try to create custom constraint annotations. For convenience, I’ve defined all implementations in a single annotation.

/** * This custom constraint annotation is used to check the parity of methods, fields, parameters, and types. * <p> * Is based on the specification@author lx
 */
@Target({METHOD, FIELD, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = Odevity.MyConstraintValidator.class)
public @interface Odevity {

    /** * Returns the default key */ for creating error messages when constraints are violated
    String message(a) default "{com.spring.mvc.config.Odevity.message}"; /** * Allows the specification validation group to which this constraint belongs */ Class
      [] groups() default {}; /** * You can assign a custom Payload object to a constraint, usually without */ Class
      [] payload() default {}; /** * OdevityMode value(); /** OdevityMode value(); /** * implementation of Validator, */ Class MyConstraintValidator implements ConstraintValidator
      
        {private OdevityMode OdevityMode;  /** @param constraintAnnotation Current Odevity annotation instance */ @override public void initialize(Odevity) constraintAnnotation) { odevityMode = constraintAnnotation.value(); } /** * @param value value of the annotated data * @param context check context * @return check whether the check, false not pass, */ @override public Boolean isValid(Long value, ConstraintValidatorContext context) { if (value == null) { return true; } boolean flag = value % 2 == 0; return flag && odevityMode == OdevityMode.EVEN || (! flag && odevityMode == OdevityMode.ODD); }} / * * * parity enumerated constants * / enum OdevityMode {/ * * * * / ODD, ODD/EVEN * * * * / EVEN; }}
      ,>Copy the code

@odevity is a custom constraint annotation to check whether the value is odd or even! Above the annotations are several meta-annotations:

  1. @Target: represents the positions that the annotation can flag, including methods, fields, parameters, types (return value types, generic types, and so on).
  2. @RetentionRUNTIME means that the annotation is recorded to the class file at compile time, but the annotation is still retained at RUNTIME, so the annotation information can be retrieved by reflection.
  3. @javax.validation.Constraint: This note isJava Bean validationA meta-annotation provided to indicate@OdevityAn annotation is a constraint annotation and is specified for validation use@OdevityValidator for annotated dataClass. If the constraint annotation can be used for more than one data type, you can specify the Class of multiple validators, each of which corresponds to a verifiable data type, and the corresponding validator will be called for different types. If two validators correspond to the same verifiable data type, an exception will be thrown.

According to the Jakarta Bean Validation API, any custom constraint annotations should contain the following three elements:

  1. message: Returns the error message created when a constraint is violated. You can use {key} to set the default key, and the default value will be automatically searched. If no value exists, the default error message will be the value of the key. The element value of the annotation can be obtained in the message with {elementName}, the value of the annotation annotation can be obtained with ${validatedValue}, or other EL expressions can be used.
  2. attribute: allows the specification validation group to which this constraint belongs.
  3. payload: You can assign a user-defined valid Payload object to a constraint. It is usually not used.

MyConstraintValidator is a constraint validator that usually needs to be implementedJakarta javax.mail of Bean Validation. The Validation. ConstraintValidator interface! Usually the validator is defined as a separate external class, which is defined directly in annotations for convenience.

ConstraintValidatorThe interface hasTwo generic parameters.The first parameterSpecify the type of annotation to validate (Odevity),The secondSpecify a data type to be checked to indicate that the validator can handle these types, in this case Long, so if the @odevity flag is on another data type, it will be thrownUnexpectedTypeExceptionException:

The implementation of the validator is very simple. The Initialize () method allows us to access the property values of the validation constraint annotations, which we can store in the fields of our custom validator, as shown in the example. The isValid() method contains the actual validation logic, with the first argument representing the value of the data to be validated. Note that the Jakarta Bean Validation specification recommends that null values be considered valid. If NULL is not a valid value for the element (considered checked), the @notnull annotation should be explicitly used.

Error message generation depends onisValid()Method returns true or false, false indicates that the check was not passed! Use pass the second parameter ConstraintValidatorContext object, you can add other error messages or completely disable the default error message generated and defines only the custom error messages, is generally not commonly used.

In addition, implementedConstraintValidatorClasses are automatically added to the Spring container for unified management, so we do not need to manually initialize or add@compent or other component annotations!

Finally, we are in the projectResources directoryNext add an internationalized error message fileValidationMessages_zh_CN.propertiesThe internal key is the default value of message in @odevity:

The element value of the annotation can be obtained with {elementName}, the value of the annotation annotation can be obtained with ${validatedValue}, and other EL expression # pairs can be used${validatedValue}. {value}"The Chinese unicode com. Spring. MVC. Config. The Odevity. Message = \ u6821 \ u9a8c \ u503c: ${validatedValue}.\u4e0d\u7b26\u5408\u6821\u9a8c\u7684\u8981\u6c42\uff1a{value}Copy the code

Adding an internationalized message file is not necessary because most projects don’t have a requirement for internationalization, so let’s just define the error message as the default value of the Message element!

4.1 Test Cases

Test controller:

@RestController
@Validated
public class MyValidationController {

    /** * Custom constraint annotation test case requiring that the parameters must be odd */
    @GetMapping("/odevity/{num}")
    public Long odevity(@PathVariable @Odevity(Odevity.OdevityMode.ODD) Long num) {
        System.out.println(num);
        returnnum; }}Copy the code

Access /odevity/1, of course check pass:

Failed to access /odevity/2:

5 Unified exception handling

If the Spring exception processing verification fails, usually three exceptions will be thrown. During unified exception processing, these three exceptions can be captured, extracted and returned to the client as result:

  1. Spring driver method check exception:ConstraintViolationException.
  2. Spring MVC JSON request parameter object internal attribute verification exception:MethodArgumentNotValidException.
  3. Spring MVC common request parameter object internal property check exception:BindException.

The specific implementation of unified exception handling will be covered in a separate article later!

Related articles:

  1. spring.io/
  2. Spring Framework 5.x learning
  3. Spring Framework 5.x source code

If you need to communicate, or the article is wrong, please leave a message directly. In addition, I hope to like, collect, pay attention to, I will continue to update a variety of Java learning blog!