Explain the SptingBoot parameter verification mechanism in detail, and the verification is no longer confusing

foreword

The Spring Validation validation framework provides a very convenient parameter validation function, which only requires @Validatedor @Validand some rule annotations to validate parameters.

I have seen many SpringBoot parameter verification tutorials on the Internet to classify them from the perspectives of "single parameter verification" and "entity class parameter verification" (or "Get method" and "Post method", which are actually the same, even This is more likely to be misunderstood). This kind of classification can easily make people feel confused: @Validatedannotations are marked on the class at one time, and before the parameters at another time; exceptions need to be handled BindExceptionagain and again ConstraintViolationException. You may remember it when you first read it, but it is easy to get confused after a while, especially when the two methods are in the same class at the same time, you don’t remember how to use them, and you may just add @Validatedannotations to all of them in the end.

This article is classified from the perspective of the verification mechanism. There are two mechanisms for parameter verification in SpringBoot, which will be controlled by the two mechanisms at the same time during execution. In addition to controlling their respective parts, the two sets of mechanisms overlap, and this part will involve issues such as priority. But once you know what the two mechanisms are, and you understand the Spring process, you'll never get confused again.

Check mechanism

These two sets of verification mechanisms, the first one is controlled by SpringMVC. This kind of validation can only be used in the "Controller" layer, and needs to be marked before the object to be verified @Valid, @Validated, or a custom annotation whose name starts 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 kind of bean can take effect as long as it is a Spring-managed bean, so the "Controller", "Service", "Dao" layers, etc. can all be verified with this parameter. An annotation needs to be marked on the class to be verified @Validated, and then if a single type of parameter @NotEmptyis verified, a verification rule annotation such as an annotation is directly marked in front of the parameter; if an object is verified, an annotation is marked in front of the object @Valid(only used here @Valid, Others cannot take effect, the reasons will be explained later), such as:

@Slf4j
@Validated
@RestController
@RequestMapping
public class ValidController {
    /**
     * 校验对象
     */
    @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;
}

Detailed explanation of SpringMVC verification mechanism

First, let's have a general understanding of the SpringMVC execution process:

  1. Receive all front-end requests through DispatcherServlet
  2. Obtain the corresponding HandlerMapping through configuration, and map the request to the handler. That is to find the corresponding Method information of the corresponding Controller according to the parsing url, http protocol, request parameters, etc.
  3. Obtain the corresponding HandlerAdapter through configuration for actual processing and calling HandlerMapping. That is, the HandlerAdapter actually calls the method of the Controller written by the user.
  4. Obtain the corresponding ViewResolver through configuration, and process the returned data obtained by the previous call.

The function of parameter verification is done in step 3. The client request generally calls the method through RequestMappingHandlerAdaptera series of configuration information and encapsulation .ServletInvocableHandlerMethod.invokeHandlerMethod()

HandlerMethod

This ServletInvocableHandlerMethodinheritance InvocableHandlerMethod, the role is responsible for calling HandlerMethod.

HandlerMethodHandlerInterceptorIt is a very important class in SpringMVC. The most common place that everyone touches is the third input parameter in the interceptor Object handler. Although this input parameter is of Objecttype, it is usually converted into HandlerMethod. It is used to encapsulate "Controller", and almost all the information that may be used when calling, such as methods, method parameters, annotations on methods, and the class to which they belong, will be processed in advance and put into this class.

HandlerMethodIt only encapsulates and stores data, and does not provide specific usage methods, so InvocableHandlerMethodit appears. It is responsible for execution HandlerMethod, and ServletInvocableHandlerMethodthe processing of return values ​​and response status codes is added on top of it.

Here is a note from the source author for these two classes:

InvocableHandlerMethodCalled HandlerMethodcode:

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. Let's take a look at this method:

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
                                            Object... providedArgs) throws Exception {
    // 1. 获取 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. 初始化参数名的查找方式或框架,如反射,AspectJ、Kotlin 等
        parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
        // 3. 如果 getMethodArgumentValues() 方法第三个传参提供了一个参数,则这里用这个参数。(正常请求不会有这个参数,SpringMVC 处理异常的时候内部自己生成的)
        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. 用对应的 HandlerMethodArgumentResolver 转换参数
            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 most important thing in the method is this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);to call HandlerMethodArgumentResolverthe implementation class of the interface to process the parameters.

HandlerMethodArgumentResolver

HandlerMethodArgumentResolverIt is also a very important component part in SpringMVC, a strategy interface for parsing method parameters into parameter values, which we often call custom parameter parser. The interface has two methods:

supportsParameterThe method user determines whether the MethodParameter is handled by this Resolver

resolveArgumentThe method is used to parse the parameters into the method's input object.

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);

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

SpringMVC itself provides a lot of HandlerMethodArgumentResolverimplementation classes, such as:

RequestResponseBodyMethodProcessor( @RequestBodyannotated parameter)

RequestParamMethodArgumentResolver( @RequestParamAnnotated parameters, or Java primitive data types that no other resolver matches)

RequestHeaderMethodArgumentResolver( @RequestHeaderMethodArgumentResolverannotated parameter)

ServletModelAttributeMethodProcessor( @ModelAttributeAnnotated parameters, or custom objects that no other resolver matches) and so on.

Let's take ServletModelAttributeMethodProcessoran example to see how it looks resolveArgumentlike:

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

    // ...
    // 获取参数名称以及异常处理等,这里省略。..

    if (bindingResult == null) {  // bindingResult 为空表示没有异常
        // 1. binderFactory 创建对应的 DataBinder
        WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
        if (binder.getTarget() != null) {
            if (!mavContainer.isBindingDisabled(name)) {
                // 2. 绑定数据,即实际注入数据到入参对象里
                bindRequestParameters(binder, webRequest);
            }
            // 3. 校验数据,即 SpringMVC 参数校验的入口
            validateIfApplicable(binder, parameter);
            if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                // 4. 检查是否有 BindException 数据校验异常
                throw new BindException(binder.getBindingResult());
            }
        }
        if (!parameter.getParameterType().isInstance(attribute)) {
            // 如果入参对象为 Optional 类型,SpringMVC 会帮忙转一下
            attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
        }
        bindingResult = binder.getBindingResult();
    }

    // 添加绑定结果到 mavContainer 中
    Map<String, Object> bindingResultModel = bindingResult.getModel();
    mavContainer.removeAttributes(bindingResultModel);
    mavContainer.addAllAttributes(bindingResultModel);

    return attribute;
}

In the code, step 4 calls validateIfApplicablethe method to see that the name is verified, and look at the code:

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    for (Annotation ann : parameter.getParameterAnnotations()) {
        // 判定是否要做校验,同时获取 Validated 的分组信息
        Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
        if (validationHints != null) {
            // 调用校验
            binder.validate(validationHints);
            break;
        }
    }
}

ValidationAnnotationUtils.determineValidationHints(ann)The method is used to determine whether the parameter object has an annotation that meets the parameter verification conditions, and returns the corresponding grouping information ( @Validatedthe grouping function).

public static Object[] determineValidationHints(Annotation ann) {
    Class<? extends Annotation> annotationType = ann.annotationType();
    String annotationName = annotationType.getName();
    // @Valid 注解
    if ("javax.validation.Valid".equals(annotationName)) {
        return EMPTY_OBJECT_ARRAY;
    }
    // @Validated 注解
    Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
    if (validatedAnn != null) {
        Object hints = validatedAnn.value();
        return convertValidationHints(hints);
    }
    // 用户自定义的以 "Valid" 开头的注解
    if (annotationType.getSimpleName().startsWith("Valid")) {
        Object hints = AnnotationUtils.getValue(ann);
        return convertValidationHints(hints);
    }
    return null;
}

Here is the SpringMVC that said at the beginning that "this kind of verification can only be used in the "Controller" layer, and needs to be marked in front of the object to be verified @Valid, @Validated, or a custom annotation whose name starts with 'Valid'" to determine whether to do the verification. test code. If it is @Validated, return @Validatedthe grouped data in it, otherwise return empty data, if there is no qualified annotation, return null.

After the verification conditions are determined, the processing group information binder.validate(validationHints);will be called , and finally the method will be called to do the actual verification logic.SmartValidatororg.hibernate.validator.internal.engine.ValidatorImpl.validateValue

in conclusion:

The verification of SpringMVC is in HandlerMethodArgumentResolverthe implementation class, and the corresponding verification rules are written in the code implemented by the resolveArgument method, and the judgment of whether to verify is determined by the ValidationAnnotationUtils.determineValidationHints(ann)origin .

However, only the resolveArgument methods ModelAttributeMethodProcessorof AbstractMessageConverterMethodArgumentResolverthese two abstract classes have written verification logic, and the implementation classes are:

ServletModelAttributeMethodProcessor ( @ModelAttributeannotated parameter, or a custom object that no other resolver matches)

HttpEntityMethodProcessor ( HttpEntityor RequestEntityobject)

RequestPartMethodArgumentResolver ( @RequestPartannotated parameter or MultipartFileclass)

RequestResponseBodyMethodProcessor ( @RequestBodyannotated object)

The parameters of annotations that are often used in development @RequestParamor the resolver of a single parameter does not implement the verification logic, but this part can also be verified in use, because this part of the verification is handed over to the verification rules of the AOP mechanism. of .

Detailed Explanation of AOP Verification Mechanism

In the "Detailed Explanation of SpringMVC Verification Mechanism" section above, it is mentioned that in the process of DispatcherServlet, there will be code to InvocableHandlerMethodcall . Let HandlerMethod's review it here:

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

This getMethodArgumentValuesmethod is analyzed above, and it will get the parameters in the request and verify the parameters required to assemble into the Method. In this section, let's see what doInvoke(args)the 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) {
        // ...
        // 一堆异常的处理,这里省略
    }
}

doInvokeGet HandlerMethodthe Method and Bean objects in it, and then call the business code in the Controller we wrote through the java native reflection function.

MethodValidationInterceptor

Since what is obtained here is the Bean object managed by Spring, it must have been "proxyed". If you want to proxy, you must have a pointcut, so let's see @Validatedwhat class the annotation is called by. It is found that a named MethodValidationInterceptorclass is called. This name is related to the verification function at first glance, and it is an interceptor. Take a look at the annotation of this class.

The comment is written very directly. The first sentence says that this is MethodInterceptorthe implementation class of AOP, which provides method-level verification functions.

MethodValidationInterceptorIt is the Advice part of the AOP mechanism, which is MethodValidationPostProcessorregistered by the class to Spring's AOP management:

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
        implements InitializingBean {

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

    // ...
    // 省略一部分 set 代码。..

    @Override
    public void afterPropertiesSet() {
        // 切点判定是否由 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) : new MethodValidationInterceptor());
    }
}

afterPropertiesSetWhen initializing the bean, a pointcut class is Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);created , and the annotations on the class are used as AOP proxy.AnnotationMatchingPointcutValidated

Therefore, as long as the beans managed by Spring can use the AOP mechanism for parameter verification, there must be Validatedannotations on the class or interface where the method to be verified is located. .

Now let's take a look at MethodValidationInterceptorthe code logic in it:

public class MethodValidationInterceptor implements MethodInterceptor {

    // ...
    // 省略构造方法和 set 代码。..

    @Override
    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // 跳过 FactoryBean 类的一些关键方法不校验
        if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
            return invocation.proceed();
        }

        // 1. 获取 Validated 里的 Group 分组信息
        Class<?>[] groups = determineValidationGroups(invocation);

        // 2. 获取校验器类
        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. 调用校验方法校验入参
            result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
        } catch (IllegalArgumentException ex) {
            // 处理对象里的泛型信息
            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. 调用校验方法校验返回值
        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]);
    }
}

The invokeproxy method here mainly does several steps:

  1. Call determineValidationGroupsthe method to obtain the Group grouping information in Validated. First look for the Validatedannotation on the method to obtain the grouping information, if not, use Validatedthe grouping information of the annotation on the class.
  2. Get the validator class, usuallyValidatorImpl
  3. Call the verification method to ExecutableValidator.validateParametersverify the input parameters. If an exception is thrown IllegalArgumentException, try to obtain its generic information and verify it again. If the parameter verification fails, an exception will be ConstraintViolationExceptionthrown
  4. Call the verification method to ExecutableValidator.validateReturnValueverify the return value. If the parameter verification fails, an exception will be ConstraintViolationExceptionthrown

To sum up: SpringMVC will call the business code corresponding to the Controller through reflection. The called class is the class that is proxied by Spring AOP, and will follow the AOP mechanism. The verification function is called in the MethodValidationInterceptorclass, the calling ExecutableValidator.validateParametersmethod verifies the input parameters, and the calling ExecutableValidator.validateReturnValuemethod verifies the return value

Summary and comparison of SpringMVC and AOP verification mechanisms

  1. SpringMVC only takes effect if there is @Valid, before the method input object @Validated, or the custom name starts with 'Valid'; AOP needs to be marked on the class first @Validated, and then the validation rule annotation (such as: @NotBlank) before the method input parameter, or the validation mark before the test object @Valid.
  2. SpringMVC HandlerMethodArgumentResolverperforms parameter verification in the implementation class, so it can only be verified at the Controller layer, and only some HandlerMethodArgumentResolverimplementation classes have verification function (if RequestParamMethodArgumentResolvernot); AOP is Spring's proxy mechanism, so as long as Spring proxy Bean Do check.
  3. At present, SpringMVC verification can only verify the input parameters of custom objects, but cannot verify the return value (now Spring HandlerMethodArgumentResolverdoes not provide this function, you can achieve it by implementing your own resolver); AOP can verify basic data types and can verify return value.
  4. SpringMVC will throw BindExceptionan exception when the verification fails ( MethodArgumentNotValidExceptionalso BindExceptiona subclass in Spring 5.3); AOP verification throws ConstraintViolationExceptionan exception when the verification fails. (Tip: So you can determine which verification process to go through the thrown exception, which is convenient for locating the problem).
  5. When verifying at the Controller layer, the SpringMVC process will be followed first, and then the AOP verification process will be followed.

Original address: Detailed explanation of the SptingBoot parameter verification mechanism, the use of verification is no longer confusing

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324134395&siteId=291194637