preface

The Spring Validation framework provides a very convenient parameter Validation feature that requires only @VALIDATED or @Valid and some rule annotations to validate parameters.

I see many SpringBoot parameter validation tutorials online that classify them as “single parameter validation” and “entity class parameter validation” (or “Get method “and “Post method”, which are actually the same, and even more misleading). This classification can be confusing: the @amount, amount, and appropriateness of the annotation are marked at the top of the class and at the front of the parameter; Abnormal BindException to be processed, and to deal with ConstraintViolationException. You may remember them at first, but over time they get confused, especially if both methods are in the same class and you don’t remember how they were used, and you may end up with an @Validated annotation on all of them.

In this paper, it is classified from the perspective of verification mechanism. SpringBoot parameter verification has two sets of mechanisms, which will be controlled by both sets of mechanisms during execution. In addition to controlling the respective parts of the two mechanisms, some of them overlap, which will involve issues such as priority. But once you know what the two mechanisms are and you understand the Spring process, you’ll never confuse them again.

Check mechanism

Of these two verification mechanisms, the first is controlled by SpringMVC. This validation can only be used at the “Controller” layer and requires @Valid, @VALIDATED, or custom annotations with names starting with ‘Valid’, such as:


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

The other is controlled by AOP. This applies to all Spring-managed beans, so the “Controller”, “Service”, “DAO” layers, etc., can all be validated with this parameter. You need to mark the @VALIDATED annotation on the Validated class, and then mark a validation rule annotation such as @NotEmpty directly before the parameter if it validates a single type parameter; If the object is validated, then the @Valid annotation is written before the object (only @Valid is used here, nothing else is Valid, for reasons that will be explained later), such as:

import javax.validation.constraints.Max; @slf4j @Validated @restController @requestMapping public class validController {/** * validateObject */ @getMapping ("get2") public void get2(@Valid ValidParam param) { log.info("param: {}", param); } /** ** @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; }

SpringMVC verification mechanism details

First, take a look at the SpringMVC execution process:

  1. All front-end requests are received through the DispatcherServlet
  2. The request is mapped to the processor by configuration to get the corresponding HandlerMapping. That is, according to the parsing URL, HTTP protocol, request parameters and so on, find the corresponding Method information of the corresponding Controller.
  3. Get the corresponding HandlerAdapter by configuration, which is used to actually process and call HandlerMapping. That is, the HandlerAdapter is actually calling the Method of the Controller that the user wrote.
  4. The corresponding ViewResolver is configured to handle the data returned from 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 SpringMVC classes. The third parameter in the HandlerInterceptor is the Object Handler. But usually it will be forcedly converted to handlerMethod. It wraps the “Controller” so that almost all of the information that might be used on a call, such as methods, method parameters, annotations on methods, and the class to which they belong, is pre-processed and placed in that class.

The HandlerMethod itself only encapsulates the stored data and does not provide a specific method for using it, so the InvocableHandlerMethod appears, which is responsible for executing the HandlerMethod. While ServletInvocableHandlerMethod based on its increased return value and the processing of the response status code.

Here is the source code author’s comment on the two classes:

The 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);
}

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

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // 1. GetMethodParameter [] Parameters = getMethodParameter (); 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. The initialization parameter name lookup or framework, such as reflection, AspectJ, Kotlin parameter. InitParameterNameDiscovery (enclosing parameterNameDiscoverer); // 3. If the third argument passed by the getMethodArgumentValues() method provides an argument, then that argument is used here. Spring [I] = parameter findProvidedArgument(parameter, providedArgs); SpringMVC (parameter, providedArgs); if (args[i] ! = null) { continue; } if (! this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); } the try {/ / 4. With 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)); } } throw ex; } } return args; }

The main method is this. Resolvers. ResolveArgument (parameter, mavContainer, request, enclosing dataBinderFactory); This line, 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. The interface has two methods: the user of the supportSparameter method determines whether the MethodParameter is handled by the Resolver, and the resolveArgument method resolves the object that is the parameter to the method.

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);

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

For SpringMVC itself provides a very much HandlerMethodArgumentResolver implementation class, Parameters such as RequestResponseBodyMethodProcessor (@ RequestBody annotations), RequestParamMethodArgumentResolver (@ RequestParam annotation parameters, Or other Resolver didn’t match the Java basic data types), RequestHeaderMethodArgumentResolver (@ RequestHeaderMethodArgumentResolver annotation parameters) Parameters, ServletModelAttributeMethodProcessor (@ ModelAttribute annotations, 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 the name of the parameter, exception handling, etc. . If (bindingResult == null) {// BinderFactory creates the corresponding DataBinder WebDataBinder = binderFactory.createBinder(webRequest, attribute, name); if (binder.getTarget() ! = null) { if (! mavContainer.isBindingDisabled(name)) { // 2. BindRequestParameters (Binder, WebRequest); BindRequestParameters (WebRequest); } validateIfApplicable(binder, parameter) validateIfApplicable(binder, parameter) SpringMVC validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { // 4. Throw new BindException(binder.getBindingResult()); throw new BindException(binder.getBindingResult()); } } if (! Parameter. GetParameterType (). IsInstance (attribute)) {/ / if the object for Optional type, Can help turn the attribute = binder for SpringMVC convertIfNecessary (binder. The getTarget (), the parameter. The getParameterType (), the parameter). } bindingResult = binder.getBindingResult(); } // Add binding results to MavContainer Map<String, Object> bindingResultModel = bindingResultModel (); mavContainer.removeAttributes(bindingResultModel); mavContainer.addAllAttributes(bindingResultModel); return attribute; }

In Step 4 of our code we call validateIfApplicable and see the name validateIfApplicable. Look at the code:

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { for (Annotation ann : Parameter. GetParameterAnnotations ()) {/ / decide whether to do check, And Validated the grouping information Object [] validationHints = ValidationAnnotationUtils. DetermineValidationHints (Ann); if (validationHints ! = null) {// Call Binder.validate (ValidationHints); break; }}}

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.valid.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); } / / the user custom annotations begin with "Valid" if (annotationType. GetSimpleName (). The startsWith (" Valid ")) {Object hints = AnnotationUtils.getValue(ann); return convertValidationHints(hints); } return null; }

Here is the SpringMVC code that says “this validation can only be used at the “Controller” layer, and needs to mark @Valid, @VALIDATED, or custom annotations with names beginning with ‘Valid’ before the object being Validated.” If @ is Validated, the amount of grouped data within @ VALIDITY is returned, otherwise empty data is returned, and if there are no qualified annotations, null is returned.

Verify criteria, then binder.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 summarize:

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:

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

HttpEntityMethodProcessor (HttpEntity or RequestEntity object),

RequestPartMethodArgumentResolver (@ RequestPart annotation parameters or MultipartFile class), RequestResponseBodyMethodProcessor (@ RequestBody annotation object)

The frequently used @RequestParam annotation parameter or Resolver of a single parameter does not implement validation logic, but it can be validated in use because it is handled by AOP validation rules.

Detailed explanation of the AOP verification mechanism

As mentioned in the section “SpringMVC verification mechanism details” above, in the process of DispatcherServlet, there is the code that invocableHandlerMethod calls 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);
}

The getMethodArgumentValues Method parsed above takes the parameters in the request and validates the parameters needed to assemble the Method. In this section we’ll see 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) { // ... // handle a bunch of exceptions.

DoInvoke gets the Method and Bean objects in HandlerMethod, and then calls them to the business code in Controller through Java native reflection function.

MethodValidationInterceptor

Since this is a Spring-managed Bean object retrieved, it must be “proxied”, and to proxied there must be a pointcut facet, depending on what classes the @Validated annotation is invoked 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.

The first line of the comment states that this is an implementation class of AOP’s MethodInterceptor that 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 the set code. . Public void afterPropertiesSet() {// Checkpoint validates the appropriateness of the annotation 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) : new MethodValidationInterceptor()); }}

Initialization afterPropertiesSet Bean, pay attention to the Pointcut Pointcut = new AnnotationMatchingPointcut (enclosing validatedAnnotationType, true); This line of code creates a AnnotationMatchingPointcut tangent point of the class, the class have Validated annotation do AOP agent.

So the first requirement for the AOP mechanism to validate is that there are Validated annotations on the class. So any Bean managed by Spring can use AOP for parameter validation.

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) throws Throwable {// Override @Nullable public Object Invoke (MethodInvocation) throws Throwable {// if (isFactoryBeanMetadataMethod(invocation.getMethod())) { return invocation.proceed(); } // 1. Gets Group information Class<? >[] groups = determineValidationGroups(invocation); / / 2. Obtain validator class ExecutableValidator execVal = this. The 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 calibration method into the result = execVal refs. ValidateParameters (target, methodToValidate, invocation. GetArguments (), groups); } the catch (IllegalArgumentException ex) {/ / processing object in the generics information 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 validation method to check the return value result. = execVal validateReturnValue (target, methodToValidate returnValue, groups); if (! result.isEmpty()) { throw new ConstraintViolationException(result); } return returnValue; } protected Class<? >[] 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() : new Class<? > [0]); }}

Here the invoke proxy method takes several steps:

  1. calldetermineValidationGroupsMethod gets the GROUP grouping information for the Validated Prioritize on the lookup methodValidatedAnnotations to get grouping information or, if not available, on the classValidatedGrouping information for annotations.
  2. Gets the validator class, usuallyValidatorImpl
  3. Calling a checksum methodExecutableValidator.validateParametersChecks the input parameter if it is thrownIllegalArgumentException

    Exception, try to get its generic information and check again. Thrown if the parameter validation failsConstraintViolationExceptionabnormal
  4. Calling a checksum methodExecutableValidator.validateReturnValueCheck the return value. Thrown if the parameter validation failsConstraintViolationExceptionabnormal

To summarize:


SpringMVC is called by reflection to the corresponding business code of the Controller. The class being called is the class being proxied by Spring AOP and will use the AOP mechanism.


The validation function is in
MethodValidationInterceptorClass, called
ExecutableValidator.validateParametersMethod validates the input parameter, calling
ExecutableValidator.validateReturnValueMethod validates the return value

Summary and comparison of SpringMVC and AOP verification mechanisms

  1. SpringMVC is available only before the method enters the object@Valid.@Validated, or a custom annotation whose name begins with ‘Valid’. AOP needs to annotate the class first@Validated

    , and then annotate validation rule annotations before the method enters the parameters (e.g.@NotBlank), or verify object pre-annotation@Valid.
  2. For SpringMVC inHandlerMethodArgumentResolverParameter validation is done in the implementation class, so validation is only valid at the Controller layer, and only partiallyHandlerMethodArgumentResolverImplementation classes have validation capabilities (e.gRequestParamMethodArgumentResolverThere is no); AOP is Spring’s proxy mechanism, so just Spring proxies beans

    You can check it.
  3. Currently SpringMVC validates only custom object input parameters, not return values (which Spring provides)HandlerMethodArgumentResolverResolver Resolver can be implemented if it is not implemented. AOP can validate basic data types, and it can validate return values.
  4. SpringMVC will throw if the validation failsBindExceptionExceptions (MethodArgumentNotValidExceptionThis is also the case in Spring5.3BindExceptionSubclass of); AOP

    A validation is thrown when the validation is not validConstraintViolationExceptionThe exception. (Tip: So you can use the exception thrown to determine which validation process to follow to locate the problem.)
  5. Validation at the Controller layer will go through the SpringMVC process first, followed by the AOP process.

SptingBoot parameter validation mechanism, using validation is no longer confusing