@ What is the difference between Validated and @Valid? Check cascade attributes (internal classes)

Posted by Japet on Tue, 30 Jul 2019 18:38:13 +0200

Every sentence

There are two big jokes in the NBA: Kobe Bryant is not talented, and James is not skilled.

Relevant Reading

[Xiaojia Java] Deep understanding of data validation: Java Bean Validation 2.0 (JSR303, JSR349, JSR380) Hibernate-Validation 6.x use case
[Home Spring] Let Controller support data validation of tiling parameters (default Spring MVC uses @Valid to validate JavaBean s only)
Spring method level data validation: @Validated + Method Validation PostProcessor elegantly completes data validation action

Interested in Spring, Scannable Code joins the wx group: `Java Senior Engineer, Architect 3 groups'(there are two-dimensional codes at the end of the article)

Preface

Last article This paper introduces how to implement elegant method-level data validation in Spring environment, and lays a foreshadowing note: how does it apply in Spring MVC (Controller layer)? This article continues with the introduction of data validation in Spring MVC.~

Maybe the little buddy can immediately think: Is this different? We use Controller at the method level, so it just applies method level checking directly. ~I won't answer this question first, but I'll just throw two questions in the right direction and you should be able to figure it out for yourself.

  1. As mentioned above, method-level checking Spring is not turned on by default, but why can you do it directly with @Valid in Spring MVC?
    1. Perhaps some of his friends said that SpringBoot might have been turned on by default, but it wasn't. Even if you use the traditional Spring MVC, you will find it is directly available. If you don't believe it, try it.
  2. Analogy: Spring MVC's Handler Interceptor is an implementation of AOP, but have you found that even if you don't start support for @EnableAspectJAutoProxy, it still works well?~

If you want to understand the two questions I have raised, it will be very difficult to understand them below. Of course, even if you know the answers to these two questions, you are advised to read on. After all: always believe that this article can bring you unexpected results~

Use examples

With regard to the use of data validation in Spring MVC, I believe that any experienced Java programmer should have no skilled players who will not use it. Previously, I simply "interviewed" most programmers even thought that data validation in Spring meant using @Validated validation in Controller to join JavaBean.~

So in the following example, you should be no stranger at all:

@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;
    }

}

@RestController
@RequestMapping
public class HelloController {

    @PostMapping("/hello")
    public Object helloPost(@Valid @RequestBody Person person, BindingResult result) {
        System.out.println(result.getErrorCount());
        System.out.println(result.getAllErrors());
        return person;
    }
}

Send a post request: / hello Content-Type=application/json, and the incoming JSON string is as follows:

{
  "name" : "fsx",
  "age" : "-1",
  "child" : {
    "age" : 1
  }
}

The console has the following printing:

2
[Field error in object 'person' on field 'child.name': rejected value [null]; codes [NotNull.person.child.name,NotNull.child.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.child.name,child.name]; arguments []; default message [child.name]]; default message [Can't do null], Field error in object 'person' on field 'age': rejected value [-1]; codes [Positive.person.age,Positive.age,Positive.java.lang.Integer,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.age,age]; arguments []; default message [age]]; default message [Must be positive]]

From the print point of view: validation takes effect (with error messages you can return to the front-end display, or locate the error page all right).

Two small details of this example must be noted:

  1. @ The RequestBody annotation cannot be omitted, otherwise the incoming json cannot complete the data binding (even if it is not bound, the validation is valid).~
  2. If the method does not write the parameter BindingResult result, the request will get 400 errors directly, because if the server fails to verify, it will throw the exception org. spring framework. web. bind. MethodArgumentNotValidException. If it's written, the caller handles it himself.~

According to my incomplete and immature statistics, this case covers more than 90% of the actual use scenarios of small partners. It is really very simple, elegant and efficient to use.~

But as an experienced programmer, although you use @Valid to gracefully complete data validation, will you find that there are still a lot of if else-based validation in your code? What's the reason? There's only one root cause: many case s can't be overridden with @Valid because it can only validate JavaBean s.
I believe you have such and such use of pain points, this article first from the principle level of analysis, and then give you the pain points you encountered reference solutions to the problem.~

Principle analysis

Controller's principle of using @Valid for easy validation of JavaBean s is quite different from that of Spring method-level validation support (analogous to the difference between Spring MVC interceptors and Spring AOP). Now let's look at this one.

Don't overlook the power of elegant code. It will double your coding efficiency, double your maintenance costs, and even double your scalability and reduce your likelihood of writing bug s.~

Recall Data Binder/Web Data Binder

If you are not familiar with Spring Data Binding Module (negligible if you have read my previous article), I suggest that you make up for it first:

  1. [Little Spring] Talk about Data Binder in Spring (Source Code Analysis)
  2. [Little Spring] Talk about data binding in Spring - Web Data Binder, Servlet Request Data Binder, Web Binding Initializer...

The DataBinder class is called Data Binding, but it's in the package of org. spring framework. validation. So Spring puts data binding and data validation together firmly, and weakens the concept and logic of data validation internally (Spring wants callers not to care about the details of data validation, but to complete it automatically. Reduce the cost of use.

We know that DataBinder mainly provides bind (Property Values pvs) and validate() methods to the outside world, as well as related (configuration) components for handling binding/validation failures:

public class DataBinder implements PropertyEditorRegistry, TypeConverter {
    ...
    @Nullable
    private AbstractPropertyBindingResult bindingResult; // It's a BindingResult
    @Nullable
    private MessageCodesResolver messageCodesResolver;
    private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor();
    // Most importantly, it is org. spring framework. validation. Validator
    // A DataBinder can hold a pair of validators. That is to say, for a Bean, it can be validated by multiple validators (of course, there is only one validator in general).
    private final List<Validator> validators = new ArrayList<>();
    
    public void bind(PropertyValues pvs) {
        MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues ? (MutablePropertyValues) pvs : new MutablePropertyValues(pvs));
        doBind(mpvs);
    }
    ...
    public void validate() {
        Object target = getTarget();
        Assert.state(target != null, "No target to validate");
        BindingResult bindingResult = getBindingResult();
    
        // Get all the validators one by one to validate the target~~~
        // Call each validator with the same binding result
        for (Validator validator : getValidators()) {
            validator.validate(target, bindingResult);
        }
    }
}

DataBinder provides two very independent atomic methods: binding + validation. They combined to complete our data binding + data validation, completely business-neutral~

There are many articles on the Internet that DataBinder continues to validate after completing data binding, which is inaccurate because it does not deal with this part of the combinatorial logic, it only provides raw capabilities.~

Time for Spring MVC to process parameters

The logic of processing parameters in Spring MVC is very complex. I have spent a lot of time talking about Spring MVC's handler for returned values: Handler Method Return Value Handler.
Handler Adapter Source Details, one of the nine web components of Spring MVC Container, takes you through Handler Method Return Value Handler

Likewise, this article focuses only on its introduction of the @RequestBody type of entry.~
Handler Method ArgumentResolver. The final implementation class for @RequestBody is RequestResponseBodyMethod Processor. Spring uses this processor to complete a series of tasks such as message converter, data binding, data validation, etc.~

RequestResponseBodyMethodProcessor

This class should be unfamiliar, as mentioned in the article recommended above for handling MVC return values: it can handle the @ResponseBody annotation return value (see its supportsReturnType() method ~)
It also has another capability: it can handle request parameters (also marked with @RequestBody its ~)
So it's both a Handler Method Return Value Handler that handles return values and a Handler Method Argument Resolver that handles input values. So it's called Processor instead of Resolver/Handler. That's the art of naming.~

// @since 3.1
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestBody.class);
    }
    // It's OK to annotate @ResponseBody annotations on classes or methods
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class));
    }
    
    // This is the entrance to the package checking process, and also the focus of this paper.
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        
        // It supports `Optional'containers
        parameter = parameter.nestedIfOptional();
        // Use message converter HttpInputMessage to convert request requests
        // Note here: For example, this example is a Person class, so after processing here, an empty Person object will be generated (reflection).
        Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());

        // Gets the name of the entry
        // Note: The name here is in lowercase, not in your method. For example, Bentley's formal name is personAAA, but the value of name is person.
        String name = Conventions.getVariableNameForParameter(parameter);

        // Only binderFactory exists to complete automatic binding and validation~
        // Here the web environment is: ServletRequest Data BinderFactory
        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);

            // Obviously, parameters are passed before binding checks are needed.
            if (arg != null) {

                // Here we complete data binding + data validation ~ ~ (both binding errors and validation errors are put into Errors)
                // Applicable: Suitable
                validateIfApplicable(binder, parameter);

                // If there is an error message hasErrors(), and only one parameter followed is not Errors type, Spring MVC will actively throw a MethodArgumentNotValidException exception to you.
                // Otherwise, the caller handles it by itself
                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                    throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                }
            }
        
            // Put the error message in to prove that the error has been checked.~~~
            // Subsequent logic will determine the key of MODEL_KEY_PREFIX~~~~
            if (mavContainer != null) {
                mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
            }
        }

        return adaptArgumentIfNecessary(arg, parameter);
    }

    // Check, if appropriate. With WebDataBinder, failure information is ultimately placed on it ~This method is the focus of this article.
    // Input: MethodParameter parameter
    protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
        // Get all the annotations tagged on this parameter (for example, @Valid and @RequestBody annotations here)
        Annotation[] annotations = parameter.getParameterAnnotations();
        for (Annotation ann : annotations) {
            // Let's first look at Muyou@Validated
            Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);

            // The key here is judgment: you can see that the @Validated annotation or the annotation name that starts with Valid is valid.
            //Note: There's no need for the @Valid annotation. In fact, you customize the annotations, and the names start with a Valid.~~~~~
            if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
                // After you get the group ing, call the binder's validate() for validation~~~~
                // As you can see, once you get a proper comment, you break down immediately.~~~
                // So if you tag @Validated and @Valid on both hosts, the effect is the same.~
                Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
                Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
                binder.validate(validationHints);
                break;
            }
        }
    }
    ...
}

In this article, we focus only on the @Valid data validation section. There are several small details about the use of @Valid, which can be summarized as follows:

  1. The name of the parameter (@RequestBody tagged entry) has nothing to do with what you write, if the entity class name is lowercase. (Arrays, collections, etc., all have their own specific names)
  2. @ Validated and @Valid can make validation work, but it's not just for their brothers: any annotation with the name "Valid" at the beginning can make data validation work.
    1. Custom annotation names start with Valid, and giving a value attribute can also specify Group Groups
    2. Individuals directly suggest using @Validated instead of using @Valid, let alone bothering themselves to customize annotations.~
  3. Spring MVC only delegates error messages to the caller when Errors(BindingResult) is an entity that only follows the @Valid annotation, otherwise (no or not immediately) it throws a MethodArgumentNotValidException exception.~

This is the basic principle of data validation using @RequestBody and @Valid. In fact, when Spring MVC processes @RequestPart annotated parameter data, it also executes the relevant logic of binding and validation. The corresponding processor is RequestPart Method ArgumentResolver, which is similar in principle to this one. It mainly deals with Multipart correlation, which is ignored in this article.~

== Here's a hint. After this article was sent out, a curious baby asked me if I could use multiple objects and all of them were marked with @RequestBody. = =
Regarding this question, let's not consider whether it's reasonable or not. Let's try to do this:

    @PostMapping("/hello")
    public Object helloPost(@Valid @RequestBody Person personAAA, BindingResult result, @Valid @RequestBody Person personBBB) {
        ...
    }

The request was wrong:

Resolved [org.springframework.http.converter.HttpMessageNotReadableException: I/O error while reading input message; nested exception is java.io.IOException: Stream closed]

The error message is well understood: the Body of the request domain can only be read once (flow can only be read once).
If you're curious, you might ask: What about URL parameters? Request links? What about the following parameters? How to encapsulate them??? Because this part is not the focus of this article, if you are interested, please go out and turn left.~

Note: The situation about using Map, List, array and other parameters to accept @RequestBody is similar, but the difference is that on the binder, the previous article on the verification of Map and List has been explained, so it will not be expanded here.

It is hoped that readers will be able to grasp this part of the content, because it is strongly associated with @InitBinder, which is more user-oriented.~~~

In practical use, the @Validated grouping check is usually used (if necessary), and then the error message is displayed friendly to the caller in combination with the handling of global exceptions.~

Example of global exception handling

When the validation fails, Spring throws a MethodArgumentNotValidException exception, which holds the validation result object BindingResult to obtain the validation failure information. For example only, for reference only:

@RestControllerAdvice
public class MethodArgumentNotValidExceptionHandler {


    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();

        StringBuilder stringBuilder = new StringBuilder();
        for (FieldError error : bindingResult.getFieldErrors()) {
            String field = error.getField();
            Object value = error.getRejectedValue();
            String msg = error.getDefaultMessage();
            String message = String.format("Error field:%s,Error value:%s,Reason:%s;", field, value, msg);
            stringBuilder.append(message).append("\r\n");
        }
        return Result.error(MsgDefinition.ILLEGAL_ARGUMENTS.codeOf(), stringBuilder.toString());
    }
}

Legacy Pain Point

Did you find that although Spring MVC provides us with an extremely convenient way to verify data, it still has some limitations: it requires that the entry to be verified be JavaBean?

Note: It's not the same requirement to request a Body. For example, if the entry of get request is received with JavaBean, the validation can still be enabled.

In practice, however, many of our Controller methods are flat, that is, the so-called flat parameters.

    @PutMapping("/hello/id/{id}/status/{status}")
    public Object helloGet(@PathVariable Integer id, @PathVariable Integer status) {
        ...
        return "hello world";
    }

In fact, especially in case of get request, @RequestParam is usually very large (such as paging query). Can we really check the case of this paving parameter through human flesh if else?
Perhaps you are interested in this issue, so refer to this article, it can provide you with solutions: [Home Spring] Let Controller support data validation of tiling parameters (default Spring MVC uses @Valid to validate JavaBean s only)

==@ The difference between Validated and @Valid==

I believe that this question is a comparison that many small partners are very concerned about. If you have miaowed this series, the answer to this question will come to the surface.

  1. @ Valid: Markup annotations for the standard JSR-303 specification that mark validation attributes and method return values for cascading and recursive validation
  2. @ Validated: Spring's annotation, a variant of the standard JSR-303, provides a grouping function that allows different validation mechanisms to be used for different groupings when participating in validation.
  3. When checking method parameters in Controller, there is no special difference between @Valid and @Validated (if group checking is not required)
  4. @ Validated annotations can be used at the class level to support Spring for parameter verification at the method level. @ Valid can be used for attribute level constraints to represent cascading checks.
  5. @ Validated can only be used on classes, methods, and parameters, while @Valid can be used on methods, fields, constructors, and parameters.

Finally, a note: Spring Boot's Web Starter has added Bean Validation and implementation dependencies, which can be used directly. But if it's a pure Spring MVC environment, please import it yourself.~

summary

This paper introduces the most commonly used data validation scenario: using @Validated to complete the entry validation of Controller, to achieve elegant processing data validation. At the same time, I hope that through this article, you can thoroughly understand the differences and links between @Validated and @Valid, so that you can be more comfortable in actual production and use.~

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 Group.
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" words, will be manually invited to join the group

Topics: PHP Spring Java JSON Bean Validation