Spring method level data validation: @Validated + Method Validation PostProcessor

Keywords: Java Spring Bean Validation Hibernate

Every sentence

In Deep Work, the author puts forward a formula: high quality output = time * concentration. So high quality output does not depend on time, but on efficiency.

Relevant Reading

[Xiaojia Java] Deep understanding of data validation: Java Bean Validation 2.0 (JSR303, JSR349, JSR380) Hibernate-Validation 6.x use case
Know more about Bean Validation: Validation Provider, Constraint Descriptor, Constraint Validator
[Xiaojia Spring] Details the core API s Spring supports for Bean Validation: Validator, Smart Validator, Local Validator FactoryBean...

<center> Interested in Spring, Sweep Code joins wx group: Java Senior Engineer, Architect 3 groups (with two-dimensional code at the end of the article)</center>

Preface

When you write business logic, do you often write a lot of empty check? For example, do you have a set of validation rules for the method entry, entry object and exit in Service or Dao layer? For example, some fields must be passed, some not; some fields must have value in the return value, some not, and so on.~

As described above, take a peek at your code and estimate that there is a lot of if else in it. This part of the logic is simple (because it has little to do with the business) but it looks dazzling (sneak out your own code, haha). When the siege key becomes bigger, you will notice that there will be a lot of duplicated code. This is part of your entry into a new company: junk code.

If you pursue clean code, even code cleanliness, such as the numerous repetitive meaningless work of if else is undoubtedly your pain point, then this article should be able to help you.
Bean Validation Check is actually designed based on DDD. Although we can program in this way of thinking incompletely, its elegant advantages are preferable. This article will introduce Spring's solution to this problem.~

Examples of effects

Before explaining, let's experience it first.~

@Validated(Default.class)
public interface HelloService {
    Object hello(@NotNull @Min(10) Integer id, @NotNull String name);
}

// The implementation classes are as follows
@Slf4j
@Service
public class HelloServiceImpl implements HelloService {
    @Override
    public Object hello(Integer id, String name) {
        return null;
    }
}

Register a processor in the container:

@Configuration
public class RootConfig {
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

Test:

@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class})
public class TestSpringBean {
    @Autowired
    private HelloService helloService;

    @Test
    public void test1() {
        System.out.println(helloService.getClass());
        helloService.hello(1, null);
    }
}

The results are as follows:

Perfect calibration checks the method of participation.

Note a small detail here: if you run this case yourself, you may get parameter names like hello.args0, and I have a formal parameter name here. Because I use Java 8's compilation parameters: - parameters. (Here's a point: if your logic depends strongly on this parameter, be sure to add compilation plug-ins to your maven and configure the compilation parameters.)

If you need to check the return value of the method, rewrite it as follows:

    @NotNull
    Object hello(Integer id);

    // The effect of this writing is the same as the above.
    //@NotNull Object hello(Integer id);

Function:

javax.validation.ConstraintViolationException: hello.<return value>: Can't do null
...

Verification is completed. With the help of Spring+JSR related constraints annotations, it is very simple and clear, and the semantics is clear and elegant to complete the method level verification (check in, check return value).
Check the wrong information that fails, and then a unified global exception handling, can make the whole project show the perfect trend. (Error messages can be obtained from the getConstraintViolations() method of the exception ConstraintViolationException)

MethodValidationPostProcessor

It is Spring's core processor for implementing Method-based JSR validation ~it allows constraints to be applied to Method input and return values, such as:

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

Official note: JSR validation annotations are written in the method. To be effective, the @Validated annotation must be used at the type level (and the validated Group can also be specified).

Another hint: This processor is very similar to Async Annotation Bean PostProcessor, which handles @Async. It is inherited from AbstractBean Factory Aware Advising PostProcessor. So if you are interested in recommending @Async's analysis blog again, you can compare watching and remembering: Spring asynchronous processing @Async's use and principle, source code analysis (@EnableAsync)

// @since 3.1
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {
    // Note: Here you mark @Valid as useless ~~Spring does not provide identification
    // Of course, you can also customize the annotations (the set method is provided below ~)
    // But note: if you customize a comment, it only decides whether to proxy or not. You can't specify a group. Oh so, there's nothing to bother you.
    private Class<? extends Annotation> validatedAnnotationType = Validated.class;
    // This is javax.validation.Validator
    @Nullable
    private Validator validator;

    // Comments that can be customized to take effect
    public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {
        Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
        this.validatedAnnotationType = validatedAnnotationType;
    }

    // Note: You can pass in a Validator by yourself, and it can be a customized Local Validator FactoryBean.
    public void setValidator(Validator validator) {
        // It is recommended that the Local Validator FactoryBean be powerful enough to generate a validator from it.
        if (validator instanceof LocalValidatorFactoryBean) {
            this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
        } else if (validator instanceof SpringValidatorAdapter) {
            this.validator = validator.unwrap(Validator.class);
        } else {
            this.validator = validator;
        }
    }
    // Of course, you can also simply and crudely provide a Validator Factory directly.~
    public void setValidatorFactory(ValidatorFactory validatorFactory) {
        this.validator = validatorFactory.getValidator();
    }


    // There is no doubt that Pointcut uses Annotation Matching Pointcut and supports internal classes.~
    // Note that @Aysnc also uses Annotation Matching Pointcut, but because it supports annotations on classes and methods, it is ultimately a composite Compposable Pointcut.
    
    // As for Advice Notification, here's the same `Method Validation Interceptor'.`~~~~
    @Override
    public void afterPropertiesSet() {
        Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
        this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
    }
    
    // This advice is to enhance the @Validation class to show that subclasses can be overridden.~
    // @since 4.2
    protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
        return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
    }
}

It's a normal BeanPostProcessor, and the time to create a proxy for a Bean is postProcessAfterInitialization(), which is returned with a proxy object when necessary after the Bean has been initialized and then managed by the Spring container ~ (same as @Aysnc)
It's easy to think that the logic about validation does not lie in it, but in the aspect notification: Method Validation Interceptor

MethodValidationInterceptor

It is an AOP coalition-type notification, which is dedicated to processing method-level data validation.

Attention to understanding method level: Method level input may be a variety of paved parameters, or it may be one or more objects

// @ since 3.1 because it checks the Method, it uses javax. validation. executable. Executable Validator
public class MethodValidationInterceptor implements MethodInterceptor {

    // javax.validation.Validator
    private final Validator validator;

    // If no validator is specified, the default validator is used.
    public MethodValidationInterceptor() {
        this(Validation.buildDefaultValidatorFactory());
    }
    public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
        this(validatorFactory.getValidator());
    }
    public MethodValidationInterceptor(Validator validator) {
        this.validator = validator;
    }


    @Override
    @SuppressWarnings("unchecked")
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
        // If it's the FactoryBean.getObject() method, don't check it.~
        if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
            return invocation.proceed();
        }

        Class<?>[] groups = determineValidationGroups(invocation);

        // Standard Bean Validation 1.1 API Executable Validator is provided in 1.1
        ExecutableValidator execVal = this.validator.forExecutables();
        Method methodToValidate = invocation.getMethod();
        Set<ConstraintViolation<Object>> result; // If the error message result exists, it will eventually be thrown in the form of ConstraintViolationException exception.

        try {
            // Input of Prior Check Method
            result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        } catch (IllegalArgumentException ex) {
            // Back to asynchrony here: Find the bridged method method and do it again
            methodToValidate = BridgeMethodResolver.findBridgedMethod(ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
            result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        if (!result.isEmpty()) { // Throw an exception if there is a mistake
            throw new ConstraintViolationException(result);
        }
        // Execute the target method to get the return value and then check the return value.
        Object returnValue = invocation.proceed();
        result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }

        return returnValue;
    }


    // Find out if there is a @Validated annotation on this method to get grouping information from it
    // Note: Although proxies can only be labeled on classes, groupings can be labeled on classes and methods.~~~~ 
    protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
        Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
        if (validatedAnn == null) {
            validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class);
        }
        return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
    }
}

The implementation of this Advice is so simple that it can't be simpler. It should be easy to understand if it's a little bit basic (I don't fully estimate that this should be the simplest).

== Use details == (important)

Although the preface has given a use example, it is only partial after all. In actual production and use, for example, the above theory is more important than some details (details often distinguish whether you are a master or not). Here, from my experience in using, I summarize the following points for your reference (basically sharing the pits I lie in):

Using @Validated to verify the Method is very simple and concise both in use and in principle. It is recommended that you use it more frequently in enterprise applications.

1. Constraint annotations (such as @NotNull) cannot be placed on entity classes

Generally speaking, most of our Service layer validation (the Controller layer generally does not give interfaces) is oriented to interface programming and use, so how should this @NotNull be placed?

Look at this example:

public interface HelloService {
    Object hello(@NotNull @Min(10) Integer id, @NotNull String name);
}

@Validated(Default.class)
@Slf4j
@Service
public class HelloServiceImpl implements HelloService {
    @Override
    public Object hello(Integer id, String name) {
        return null;
    }
}

Constraints are written on implementation classes, and according to what we call experience, it should be no problem. But the operation:

javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method HelloServiceImpl#hello(Integer) redefines the configuration of HelloService#hello(Integer).

    at org.hibernate.validator.internal.metadata.aggregated.rule.OverridingMethodMustNotAlterParameterConstraints.apply(OverridingMethodMustNotAlterParameterConstraints.java:24)
...

Re-comment 3: Be sure to note that this exception is javax.validation.ConstraintDeclarationException, not error checking error exception javax.validation.ConstraintViolationException. Be sure to distinguish between global exception capturing and global exception capturing~

The exception message is that when parameter constraint configuration checks constraints on method entry, if the @Override parent class/interface method is used, the input constraints can only be written on the parent class/interface.~~~

As for why it can only be written at the interface, this specific reason is actually related to the implementation product of Bean Validation, such as the Hibernate check used, for the reasons mentioned above: Overridding Method Must NotAlter Parameter Constraints

One more thing to note: if the constraints on class writing are exactly the same as the interfaces, that's OK. For example, if the above implementation class is written like this, there is no problem to complete the normal verification:

    @Override
    public Object hello(@NotNull @Min(10) Integer id, @NotNull String name) {
        return null;
    }

Although normal work can complete the verification, but need to understand the four words exactly the same. Simply put, changing 10 to 9 will report ConstraintDeclarationException exceptions, not to mention removing a comment (no matter how many fields there are, if you write one, you have to be exactly the same).

Regarding the @Override method check return value: ConstraintDeclarationException will not be thrown even if it is written in the implementation class
In addition, the @Validated annotation is written on the implementation class/interface.~

Finally, you should realize for yourself that if the entry test fails, the method will not be implemented. But if the return value check is executed (even if it fails), the method body must be executed.~~

2. What types of @NotEmpty/@NotBlank can only be used?

The purpose of presenting this detail is that constraint annotations can not be used on all types. For example, if you let @NotEmpty verify the Object type, it will report the following error:

javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.NotEmpty' validating type 'java.lang.Object'. Check configuration for 'hello.<return value>'

It should be emphasized that if the annotation verifies the return value in the method, then the method body has been executed, unlike Constraint Declaration Exception.~

These two annotations are briefly described in accordance with official documents as follows. @ NotEmpty can only be marked in the following types

  1. CharSequence
  2. Collection
  3. Map
  4. Array

Note: It's empty, but it's not.

@ NotBlank can only be used on CharSequence, which is a new annotation to Bean Validation 2.0~

3. There are annotations on interfaces and implementation classes, whichever is the standard?

There is an implicit condition for this problem: this is only possible if the validation method returns a value.

public interface HelloService {
    @NotEmpty String hello(@NotNull @Min(10) Integer id, @NotNull String name);
}

@Slf4j
@Service
@Validated(Default.class)
public class HelloServiceImpl implements HelloService {
    @Override
    public @NotNull String hello(Integer id, String name) {
        return "";
    }
}

Running case, helloService.hello(18, "fsx"); print as follows:

javax.validation.ConstraintViolationException: hello.<return value>: Can't be empty
...

At this point, there may be small partners who will come to the conclusion early: when there are simultaneous, the constraints of the interface prevail.
So, I just slightly revise the return value. Would you like to see it again???

    @Override
    public @NotNull String hello(Integer id, String name) {
        return null; // Change the return value to null
    }

Rerun:

javax.validation.ConstraintViolationException: hello.<return value>: Can't be empty, hello.<return value>: Can't do null
...

Through the printed information, the conclusion naturally does not need me much. But there's a point here: make bold guesses and be careful to verify.

4. How to verify cascade attributes?

In practice, in most cases, our method is an object (even if there are objects in the object), rather than a single paved parameter, so we introduce an example of cascade attribute checking.

@Getter
@Setter
@ToString
public class Person {

    @NotNull
    private String name;
    @NotNull
    @Positive
    private Integer age;

    @Valid // Let InnerChild's attributes also participate in validation
    @NotNull
    private InnerChild child;

    @Getter
    @Setter
    @ToString
    public static class InnerChild {
        @NotNull
        private String name;
        @NotNull
        @Positive
        private Integer age;
    }

}

public interface HelloService {
    String cascade(@NotNull @Valid Person father, @NotNull Person mother);
}

@Slf4j
@Service
@Validated(Default.class)
public class HelloServiceImpl implements HelloService {
    @Override
    public String cascade(Person father, Person mother) {
        return "hello cascade...";
    }
}

Running test cases:

    @Test
    public void test1() {
        helloService.cascade(null, null);
    }

The output is as follows:

cascade.father: not null, cascade.mother: not null

Here's a note: If you don't add @NotNull before father, the only message printed is: cascade.mother: can't be null.

I revamped the test case as follows, so you can continue to feel it:

    @Test
    public void test1() {
        Person father = new Person();
        father.setName("fsx");
        Person.InnerChild innerChild = new Person.InnerChild();
        innerChild.setAge(-1);
        father.setChild(innerChild);

        helloService.cascade(father, new Person());
    }

The error message is as follows (please observe and analyze the reason carefully):

cascade.father.age: not null, cascade.father.child.name: not null, cascade.father.child.age: must be positive

Think: Why are all the relevant attributes and sub-attributes of mother not checked?

5. Cyclic dependence

As mentioned above, Spring's handling of @Validated is similar to the proxy logic of @Aysnc. With previous experience, it is easy to imagine that it also has problems like this: for example, HelloService's A method wants to call this class's B method, but obviously I hope that the method verification of B method will be effective, so one of them. This is done by injecting itself, using its own proxy object to invoke:

public interface HelloService {
    Object hello(@NotNull @Min(10) Integer id, @NotNull String name);
    String cascade(@NotNull @Valid Person father, @NotNull Person mother);
}

@Slf4j
@Service
@Validated(Default.class)
public class HelloServiceImpl implements HelloService {

    @Autowired
    private HelloService helloService;

    @Override
    public Object hello(@NotNull @Min(10) Integer id, @NotNull String name) {
        helloService.cascade(null, null); // Call this class of methods
        return null;
    }

    @Override
    public String cascade(Person father, Person mother) {
        return "hello cascade...";
    }
}

Running test cases:

    @Test
    public void test1() {
        helloService.hello(18, "fsx"); // Entry method validation passes, internal calls to cascade method hope to continue to be validated
    }

Operation error:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'helloServiceImpl': Bean with name 'helloServiceImpl' has been injected into other beans [helloServiceImpl] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean
...

This misinformation should not be unfamiliar. This phenomenon has been explained in great detail before and many solutions have been provided, so we will skip it here.

If the reasons for this question and the solutions are not clear, please move here: [Xiaojia Spring] Use @Async asynchronous annotation to cause the Bean to start reporting BeanCurrentlyIn Creation Exception exceptions during cyclic dependency and provide solutions

Although I will not mention the solution here, I will provide the printout of the operation after the problem has been solved, and provide the debugging reference for the small partners. This is very warm-hearted.

Javax. validation. Constraint Violation Exception: cascade. mother: cannot be null, cascade.father: cannot be null
...

summary

This article introduces Spring's ability to provide us with method-level validation. Using this method to complete most of the basic validation work in enterprise applications can make our code more concise, controllable and extensible. So I recommend using and diffusing it.~

At the end of the article, it is necessary to emphasize that the @Valid annotation used in the verification of the above cascaded attributes can not be replaced by @Validated, and will not be effective.
As for the question that I have a small partner who believes in him personally: Why is it possible for him to use @Valid and @Validated in the Controller method, and all the answers agreed on the Internet are available, almost??? Or that sentence: This is the focus of the next article, please continue to pay attention to it.~

Let's talk a little about its drawbacks: because the check fails, it eventually uses the throw exception method to interrupt, so there is a loss of efficiency. but does your application really need to consider this extreme performance issue? That's what you should think about.~

Knowledge exchange

If the format of the article is confused, click: Text Link-Text Link-Text Link-Text Link-Text Link-Text Link

== The last: If you think this is helpful to you, you might as well give a compliment. Of course, sharing your circle of friends so that more small partners can see it is also authorized by the author himself.~==

** If you are interested in technical content, you can join the wx group: Java Senior Engineer and Architect.
If the group two-dimensional code fails, Please add wx number: fsx641385712 (or scan the wx two-dimensional code below). And note: "java into the group" will be manually invited to join the group**

Posted by fighnight on Fri, 26 Jul 2019 21:18:07 -0700