preface

The Spring Validation framework provides very convenient parameter verification. You only need @Validated or @Valid and some rule annotations to verify parameters.

I see many online SpringBoot parameter verification tutorial to “single parameter verification” and “entity class parameter verification” this two Angle to classify (or “Get method “and “Post method” classification, in fact is the same, or even more misleading). It’s easy to get confused with this classification: the @validated annotation is labeled one time above the class and the next before the parameter; Abnormal BindException to be processed, and to deal with ConstraintViolationException. You may remember it at first, but after a while it will get confused, especially when you have two methods in the same class, so you may even have @Validated annotated everything.

In this paper, from the point of view of verification mechanism classification, SpringBoot parameter verification has two mechanisms, execution will be controlled by two mechanisms at the same time. In addition to controlling their own parts, the two mechanisms overlap, which in turn involves issues such as priorities. But once you know what the two mechanisms are, and understand the Spring process, there is no confusion.

Check mechanism

The first of these two verification mechanisms is controlled by SpringMVC. This verification can only be used at the “Controller” layer. The verified object must be marked with @Valid, @Validated, or a user-defined annotation whose name starts with ‘Valid’, for example:

@Slfj
@RestController
@RequestMapping
public class ValidController {
    @GetMapping("get1")
    public void get1(@Validated ValidParam param) {
        log.info("param: {}", param); }}@Data
public class ValidParam {
    @NotEmpty
    private String name;
    @Min(1)
    private int age;
}
Copy the code

The other is controlled by AOP. This is valid for any spring-managed Bean, so the “Controller”, “Service”, “Dao” layers can all be verified with this parameter. The class to be verified needs to be annotated with @Validated. Then, if a parameter of a single type is verified, a validation rule annotation such as @NOtempty is directly added to the parameter. If you are validating an object, the @VALID annotation is used before the object (only @VALID is used here, for reasons explained below), as in:

@Slf4j
@Validated
@RestController
@RequestMapping
public class ValidController {
    /** * Check object */
    @GetMapping("get2")
    public void get2(@Valid ValidParam param) {
        log.info("param: {}", param);
    }

    /** ** Check parameters */
    @GetMapping("get3")
    public void get3(@NotEmpty String name, @Max(1) int age) {
        log.info("name: {}, age: {}", name, age); }}@Data
public class ValidParam {
    @NotEmpty
    private String name;
    @Min(1)
    private int age;
}
Copy the code

SpringMVC verification mechanism in detail

First, take a look at the SpringMVC execution flow:

  1. Receive all front-end requests via DispatcherServlet
  2. The request is mapped to the handler by getting the corresponding HandlerMapping through configuration. That is, according to the parsing URL, HTTP protocol, request parameters and so on to find the corresponding Controller corresponding Method information.
  3. Obtain the corresponding HandlerAdapter through configuration for actual processing and calling HandlerMapping. That is, the HandlerAdapter actually calls the Method of the user-written Controller.
  4. Obtain the corresponding ViewResolver by configuring it to process the returned data obtained in the previous call.

The function of the parameter calibration is done in step 3, the client request usually by RequestMappingHandlerAdapter series of configuration information and encapsulation, The final call to ServletInvocableHandlerMethod. InvokeHandlerMethod () method.

HandlerMethod

The ServletInvocableHandlerMethod inherited InvocableHandlerMethod, call HandlerMethod role is responsible for.

HandlerMethod is one of the most important classes in SpringMVC. The HandlerMethod is the third entry parameter of the HandlerInterceptor, Object handler. But it’s always going to be a strong HandlerMethod. It is used to encapsulate the “Controller”, and almost all the information that might be used in the call, such as methods, method parameters, annotations on methods, and classes, are pre-processed and placed in this class.

The HandlerMethod itself only encapsulates the stored data and does not provide a specific method to use it, so the InvocableHandlerMethod comes in and executes the HandlerMethod, While ServletInvocableHandlerMethod based on its increased return value and the processing of the response status code.

Here are the author’s comments on these two classes:

InvocableHandlerMethod calls the HandlerMethod code:

public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
                                Object... providedArgs) throws Exception {
    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    if (logger.isTraceEnabled()) {
        logger.trace("Arguments: " + Arrays.toString(args));
    }
    return doInvoke(args);
}
Copy the code

The first line getMethodArgumentValues() is the method that maps the request parameters to Java objects. Take a look at this method:

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
                                            Object... providedArgs) throws Exception {
    // 1. Obtain the input information of the Method
    MethodParameter[] parameters = getMethodParameters();
    if (ObjectUtils.isEmpty(parameters)) {
        return EMPTY_ARGS;
    }

    Object[] args = new Object[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
        MethodParameter parameter = parameters[i];
        // 2. Initialize the lookup method or framework for parameter names, such as Reflection, AspectJ, Kotlin, etc
        parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
        // 3. If the third parameter to getMethodArgumentValues() provides a parameter, this parameter is used. (Normal requests don't have this parameter, SpringMVC generates it internally when handling exceptions)
        args[i] = findProvidedArgument(parameter, providedArgs);
        if(args[i] ! =null) {
            continue;
        }
        if (!this.resolvers.supportsParameter(parameter)) {
            throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
        }
        try {
            / / 4. Use the corresponding HandlerMethodArgumentResolver transformation parameters
            args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
        } catch (Exception ex) {
            // Leave stack trace for later, exception may actually be resolved and handled...
            if (logger.isDebugEnabled()) {
                String exMsg = ex.getMessage();
                if(exMsg ! =null&&! exMsg.contains(parameter.getExecutable().toGenericString())) { logger.debug(formatArgumentError(parameter, exMsg)); }}throwex; }}return args;
}
Copy the code

Methods in the main is this. Resolvers. ResolveArgument (parameter, mavContainer, request, enclosing dataBinderFactory); , the call HandlerMethodArgumentResolver interface implementation class processing parameters.

HandlerMethodArgumentResolver

HandlerMethodArgumentResolver is also a very important component part of, the SpringMVC, used in the method parameters into the strategy of parameter value interface, we often say that the custom parameter parser. Interfaces have two methods:

The supportsParameter method user determines whether the MethodParameter is handled by the Resolver

The resolveArgument method is used to parse arguments into method input objects.

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);

    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                           NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
Copy the code

For SpringMVC itself provides a very much HandlerMethodArgumentResolver implementation class, such as:

The parameters of RequestResponseBodyMethodProcessor (@ RequestBody annotations)

RequestParamMethodArgumentResolver (@ RequestParam annotation parameters, or other Resolver matching Java basic data types)

The parameters of RequestHeaderMethodArgumentResolver (@ RequestHeaderMethodArgumentResolver annotations)

ServletModelAttributeMethodProcessor (@ ModelAttribute annotation parameters, or other custom Resolver matching objects), and so on.

We ServletModelAttributeMethodProcessor, for example, take a look at how its resolveArgument:

public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                                    NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

    // ...
    // Get parameter names and exception handling, which are omitted here. .

    if (bindingResult == null) {  // If bindingResult is empty, there is no exception
        BinderFactory creates the corresponding DataBinder
        WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
        if(binder.getTarget() ! =null) {
            if(! mavContainer.isBindingDisabled(name)) {// 2. Bind data, that is, actually inject data into the input object
                bindRequestParameters(binder, webRequest);
            }
            // 3. Validation data, i.e. entry to SpringMVC parameter validation
            validateIfApplicable(binder, parameter);
            if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                // 4. Check whether there are BindException data verification exceptions
                throw newBindException(binder.getBindingResult()); }}if(! parameter.getParameterType().isInstance(attribute)) {// If the input object is of type Optional, SpringMVC will help with that
            attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
        }
        bindingResult = binder.getBindingResult();
    }

    // Add the binding result to mavContainer
    Map<String, Object> bindingResultModel = bindingResult.getModel();
    mavContainer.removeAttributes(bindingResultModel);
    mavContainer.addAllAttributes(bindingResultModel);

    return attribute;
}
Copy the code

Step 4: Call validateIfApplicable ();

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    for (Annotation ann : parameter.getParameterAnnotations()) {
        // Determine whether to perform verification and obtain the Validated grouping information
        Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
        if(validationHints ! =null) {
            // Call verification
            binder.validate(validationHints);
            break; }}}Copy the code

ValidationAnnotationUtils. DetermineValidationHints (Ann) method is used to determine whether the parameter object parameters calibration condition annotation, and returns the corresponding packet information (@ the grouping of Validated function).

public static Object[] determineValidationHints(Annotation ann) {
    Class<? extends Annotation> annotationType = ann.annotationType();
    String annotationName = annotationType.getName();
    / / @ Valid annotation
    if ("javax.validation.Valid".equals(annotationName)) {
        return EMPTY_OBJECT_ARRAY;
    }
    / / @ Validated annotation
    Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
    if(validatedAnn ! =null) {
        Object hints = validatedAnn.value();
        return convertValidationHints(hints);
    }
    // User-defined annotation starting with "Valid"
    if (annotationType.getSimpleName().startsWith("Valid")) {
        Object hints = AnnotationUtils.getValue(ann);
        return convertValidationHints(hints);
    }
    return null;
}
Copy the code

This validation can only be used at the “Controller” layer. It requires an annotation with @Valid, @Validated, or a user-defined name starting with ‘Valid’ before the object being Validated. If it is @Validated, group data in @ validation is returned; otherwise, null data is returned; if there is no qualified annotation, null is returned.

Validate (validationHints); Will call to SmartValidator grouping processing information, the final call to org.. Hibernate validator. Internal. Engine. ValidatorImpl. ValidateValue method to do the actual validation logic.

To sum up:

Check for SpringMVC is in HandlerMethodArgumentResolver implementation class, resolveArgument method implementation code written in the corresponding validation rules, The determination of whether validation is by ValidationAnnotationUtils determineValidationHints (Ann) to decide.

However only ModelAttributeMethodProcessor, AbstractMessageConverterMethodArgumentResolver both resolveArgument method to write the validation logic of abstract classes, The implementation classes are as follows:

ServletModelAttributeMethodProcessor (@ ModelAttribute annotation parameters, or other custom Resolver matching object)

HttpEntityMethodProcessor (HttpEntity or RequestEntity object)

RequestPartMethodArgumentResolver (@ RequestPart annotation parameters or MultipartFile class)

RequestResponseBodyMethodProcessor (@ RequestBody annotation object)

The @RequestParam annotated arguments or resolvers for individual arguments that are often used in development do not implement validation logic, but they can be validated in use because the validation is handled by the AOP mechanism’s validation rules.

AOP verification mechanism in detail

In the DispatcherServlet process, there will be code for InvocableHandlerMethod to call HandlerMethod. Here is a review:

public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
                                Object... providedArgs) throws Exception {
    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    if (logger.isTraceEnabled()) {
        logger.trace("Arguments: " + Arrays.toString(args));
    }
    return doInvoke(args);
}
Copy the code

The getMethodArgumentValues Method, as analyzed above, takes the parameters of the Request and validates the parameters needed to assemble it into Method. This section looks at what the doInvoke(ARgs) Method does.

protected Object doInvoke(Object... args) throws Exception {
    Method method = getBridgedMethod();
    ReflectionUtils.makeAccessible(method);
    try {
        if (KotlinDetector.isSuspendingFunction(method)) {
            return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
        }
        return method.invoke(getBean(), args);
    } catch (IllegalArgumentException ex) {
        // ...
        // A bunch of exception handling is omitted here}}Copy the code

The doInvoke retrieves the Method and Bean objects in the HandlerMethod and then invokes the business code in the Controller we wrote via Java native reflection.

MethodValidationInterceptor

Since this is a Spring-managed Bean object, it must be “proxied”. Proxiing requires a pointcut, so let’s look at what class the @Validated annotation is called by. Find a class named MethodValidationInterceptor call arrived, a see this name and check function, and is a blocker, look at the comments of a class.

This is the implementation class for AOP’s MethodInterceptor, which provides method-level validation.

MethodValidationInterceptor notice (Advice) part of the AOP mechanism, by MethodValidationPostProcessor class registered in Spring AOP management:

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
        implements InitializingBean {

    private Class<? extends Annotation> validatedAnnotationType = Validated.class;

    // ...
    // omit part of set code. .

    @Override
    public void afterPropertiesSet(a) {
        // Whether the tangent point is annotated with Validated
        Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
        this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
    }

    protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
        return(validator ! =null ? new MethodValidationInterceptor(validator) : newMethodValidationInterceptor()); }}Copy the code

Initialization afterPropertiesSet Bean, Pointcut Pointcut = new AnnotationMatchingPointcut (enclosing validatedAnnotationType, true); Created a AnnotationMatchingPointcut tangent point of the class, the class have Validated annotation do AOP agent.

So,, any beans managed by Spring can use the AOP mechanism to validate parameters and have a Validated class or interface on which the verified method belongs.

Now take a look at the code in the MethodValidationInterceptor logic:

public class MethodValidationInterceptor implements MethodInterceptor {

    // ...
    // omit the constructor and set code. .

    @Override
    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // Skip some key methods of the FactoryBean class without validation
        if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
            return invocation.proceed();
        }

        Obtain the Group information in the Validated valueClass<? >[] groups = determineValidationGroups(invocation);// 2. Obtain the validator class
        ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation.getMethod(); Set<ConstraintViolation<Object>> result; Object target = invocation.getThis(); Assert.state(target ! =null."Target must not be null");

        try {
            // 3. Call the verification method to verify the input parameter
            result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
        } catch (IllegalArgumentException ex) {
            // Process generic information in objects
            methodToValidate = BridgeMethodResolver.findBridgedMethod(
                    ClassUtils.getMostSpecificMethod(invocation.getMethod(), target.getClass()));
            result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
        }
        if(! result.isEmpty()) {throw new ConstraintViolationException(result);
        }

        Object returnValue = invocation.proceed();
        // 4. Call the verification method to verify the return value
        result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups);
        if(! result.isEmpty()) {throw new ConstraintViolationException(result);
        }

        return returnValue;
    }

    protectedClass<? >[] determineValidationGroups(MethodInvocation invocation) { Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);if (validatedAnn == null) { Object target = invocation.getThis(); Assert.state(target ! =null."Target must not be null");
            validatedAnn = AnnotationUtils.findAnnotation(target.getClass(), Validated.class);
        }
        return(validatedAnn ! =null ? validatedAnn.value() : newClass<? > [0]); }}Copy the code

Here the Invoke proxy method mainly does a few steps:

  1. calldetermineValidationGroupsMethod Obtain the Group information in “Validated”. Find the method firstValidatedAnnotations are used to obtain grouping information, or class values if none are availableValidatedGrouping information for annotations.
  2. Gets the validator class, usuallyValidatorImpl
  3. Calling validation methodsExecutableValidator.validateParametersValidates the input parameter if thrownIllegalArgumentExceptionException, try to get its generic information to verify again. If the parameter verification fails, it will be thrownConstraintViolationExceptionabnormal
  4. Calling validation methodsExecutableValidator.validateReturnValueVerify the return value. If the parameter verification fails, it will be thrownConstraintViolationExceptionabnormal

To summarize: SpringMVC calls the business code corresponding to the Controller through reflection, and the called class is the class propped up by Spring AOP, which goes through the AOP mechanism. Check function is MethodValidationInterceptor class invokes the call ExecutableValidator. ValidateParameters method check into, Call ExecutableValidator validateReturnValue method to check the return value

Summary and comparison of SpringMVC and AOP verification mechanism

  1. SpringMVC only has one before the method entry object@Valid.@Validated, or a custom annotation whose name starts with ‘Valid’ is Valid; AOP requires class annotation first@Validated, and then annotate the validation rule before the method entry (e.g.@NotBlank), or before the validation object@Valid.
  2. For SpringMVC inHandlerMethodArgumentResolverThe implementation class does parameter validation, so the validation only works at the Controller layer, and only partiallyHandlerMethodArgumentResolverImplementation classes have validation capabilities (e.gRequestParamMethodArgumentResolverNo); AOP is Spring’s proxy mechanism, so as long as Spring proxy beans can do validation.
  3. Currently SpringMVC validates only incoming arguments to custom objects, not return values (which Spring now provides)HandlerMethodArgumentResolverResolver (Resolver, Resolver, Resolver); AOP can validate base data types, and it can validate return values.
  4. SpringMVC throws if the validation failsBindExceptionExceptions (MethodArgumentNotValidExceptionAlso changed in Spring5.3BindExceptionSubclass); AOP validation is thrown if the validation failsConstraintViolationExceptionThe exception. (Tip: Therefore, you can determine which verification process to go through by throwing exceptions, which is convenient to locate problems).
  5. The SpringMVC process will be followed by the AOP verification process at the Controller layer.

SptingBoot parameter verification mechanism, the use of verification is no longer chaotic