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

zzzzbw
中文

Preface

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

I read many SpringBoot parameter verification tutorials on the based on 160f7b869916bc "Single Parameter Verification" and "Entity Class Parameter Verification" . The same, even this is more misleading).
This classification is easy to make people feel confusing: the annotation @Validated will be marked on the class for a while, and then marked before the parameter; the exception must be dealt with BindException ConstraintViolationException must be dealt with.
You may still remember it when you first read it, but it will be easy to remember it after a while, especially when the two methods are in the same class at the same time, you can't remember how to use it. In the end, you may simply add @Validated annotations.

This article will classify from the perspective of verification mechanism, SpringBoot's parameter verification has two sets of mechanisms, which will be controlled by the two sets of mechanisms at the time of execution. In addition to controlling their respective parts, the two mechanisms are partially overlapped, and this part will involve issues such as priority. But as long as you know what the two mechanisms are, and understand
The Spring process will never be confused anymore.

Verification mechanism

Of these two verification mechanisms, the first is controlled by SpringMVC. This kind of verification can only be used in the "Controller" layer. It is necessary to mark @Valid , @Validated front of the object to be verified, 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" layer, etc. can all be verified with this parameter. Need to mark @Validated
Annotation, and then if you verify a single type of parameter, mark @NotEmpty directly in front of the parameter; if you verify the object, mark the @Valid annotation in front of the object (here only @Valid can be used, and the others cannot take effect. 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, get a general understanding of the SpringMVC execution process:

  1. Receive all requests initiated by the front end through DispatcherServlet
  2. Obtain the corresponding HandlerMapping through configuration and map the request to the processor. That is, according to the analysis of url, http protocol, request parameters, etc., find the information of the corresponding Method of the corresponding Controller.
  3. Obtain the corresponding HandlerAdapter through configuration, which is used 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 return data obtained by the previous call.

Parameter calibrating function is done at step 3, the client requests typically by RequestMappingHandlerAdapter series configuration information and packaging, the final call to ServletInvocableHandlerMethod.invokeHandlerMethod() method.

HandlerMethod

This ServletInvocableHandlerMethod inherits InvocableHandlerMethod , and its role is to call HandlerMethod .

HandlerMethod is a very important class in SpringMVC. The place that everyone most often touches is the third entry HandlerInterceptor Object handler , although this entry is Object
Type, but usually will be forced to HandlerMethod . It is used to encapsulate the "Controller". Almost all the information that may be used when invoking, such as methods, method parameters, method annotations, and belonging classes, will be processed in advance and placed in this class.

HandlerMethod itself only encapsulates and stores data and does not provide specific usage methods, so InvocableHandlerMethod appears, which is responsible for executing HandlerMethod , and ServletInvocableHandlerMethod adds processing of return values and response status codes on its basis.

Here are the comments of the source author on these two classes:

InvocableHandlerMethod calls the code of HandlerMethod

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 of mapping 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 method in the method is this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); , which calls the implementation class of the HandlerMethodArgumentResolver

HandlerMethodArgumentResolver

HandlerMethodArgumentResolver is also a very important component part of SpringMVC. It is a strategy interface used to parse method parameters into parameter values, which is what we often call a custom parameter parser. The interface has two methods:

supportsParameter method user determines whether the MethodParameter is processed by this Resolver

resolveArgument method is used to parse the parameters into the input object of the method.

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 HandlerMethodArgumentResolver implementation classes, such as:

RequestResponseBodyMethodProcessor ( @RequestBody annotated by 060f7b86991b38)

RequestParamMethodArgumentResolver ( @RequestParam annotated by 060f7b86991b4d, or Java basic data types that are not matched by other Resolver)

RequestHeaderMethodArgumentResolver ( @RequestHeaderMethodArgumentResolver annotated by 060f7b86991b63)

ServletModelAttributeMethodProcessor ( @ModelAttribute annotated by 060f7b86991b84, or custom objects that are not matched by other resolvers) and so on.

Let's take ServletModelAttributeMethodProcessor as an example to see how its resolveArgument like:

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 step 4 of the code, call the validateIfApplicable method to check the name. Take a 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) method is used to determine whether this parameter object has a comment that meets the parameter verification conditions, and returns the corresponding grouping information (the grouping function of @Validated

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 "this kind of verification can only be used in the "Controller" layer, and it needs to be marked with @Valid , @Validated , or a custom name starting with "Valid"" in front of the object to be verified. Do the verification code.
If it is @Validated it returns @Validated , otherwise it returns empty data. If there is no comment that meets the conditions, it returns null.

After determining the verification conditions, then binder.validate(validationHints); will call SmartValidator process the grouping information, and finally call the org.hibernate.validator.internal.engine.ValidatorImpl.validateValue method to do the actual verification logic.

in conclusion:

The verification of SpringMVC is in HandlerMethodArgumentResolver , and the corresponding verification rules are written in the code implemented by the ValidationAnnotationUtils.determineValidationHints(ann) . The judgment of whether to verify is determined by 060f7b86991df9.

However, only the ModelAttributeMethodProcessor AbstractMessageConverterMethodArgumentResolver the two abstract classes 060f7b86991e1b and 060f7b86991e1d have written verification logic. The implementation classes are:

ServletModelAttributeMethodProcessor ( @ModelAttribute annotated by 060f7b86991e3d, or custom objects that are not matched by other Resolver)

HttpEntityMethodProcessor ( HttpEntity or RequestEntity object)

RequestPartMethodArgumentResolver ( @RequestPart annotated by MultipartFile or 060f7b86991e7b class)

RequestResponseBodyMethodProcessor ( @RequestBody annotated by 060f7b86991e95)

@RequestParam annotated parameter or the Resolver of a single parameter that is often used in development does not implement the verification logic, but this part can also be verified in use, because this part of the verification is passed to the AOP mechanism for verification rule processing.

Detailed AOP verification mechanism

In the "Detailed Explanation of SpringMVC Verification Mechanism" section mentioned above, in the DispatcherServlet process, there will be InvocableHandlerMethod calling HandlerMethod code, 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 is analyzed above, and the parameters in the request will be obtained and verified to be assembled into the parameters required by the Method. In this section, look 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) {
        // ...
        // 一堆异常的处理,这里省略
    }
}

doInvoke obtains HandlerMethod , and then calls the business code in the Controller we wrote through the Java native reflection function.

MethodValidationInterceptor

Since what is obtained here is a Bean object managed by Spring, it must have been "proxy". To be proxied, there must be a point of contact. Then look at @Validated annotation has been called. Found that a MethodValidationInterceptor was called,
At first glance, this name is related to the verification function and is an interceptor. Take a look at the annotations of this class.

The comment is very straightforward. The first sentence says that this is MethodInterceptor , which provides method-level verification functions.

MethodValidationInterceptor is regarded as the Advice part of the AOP mechanism, which is registered to Spring's AOP management MethodValidationPostProcessor

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

afterPropertiesSet initializes the Bean, Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); creates a AnnotationMatchingPointcut Validated , which will make the class with 060f7b86992061 annotated as an AOP proxy.

So as long as it is a bean managed by Spring, the AOP mechanism can be used for parameter verification, and the class or interface of the method to be verified must have Validated annotations. .

Now look at the code logic in MethodValidationInterceptor

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

Here invoke proxy method mainly does several steps:

  1. Call the determineValidationGroups method to get the Group information in Validated. First look for the Validated annotation on the method to obtain the grouping information, if not, use the grouping information annotated with Validated
  2. Get the validator class, usually ValidatorImpl
  3. Call the verification method ExecutableValidator.validateParameters verify the input parameters, if it throws IllegalArgumentException
    Exception, try to obtain its generic information and verify again. If the parameter verification fails, ConstraintViolationException exception will be thrown
  4. Call the verification method ExecutableValidator.validateReturnValue verify the return value. If the parameter verification fails, ConstraintViolationException exception will be thrown
in conclusion:
Spring MVC will call the corresponding business code of the Controller through reflection, and the called class is the class proxied by Spring AOP, which will use the AOP mechanism.
The verification function is MethodValidationInterceptor class, the ExecutableValidator.validateParameters parameters, and the ExecutableValidator.validateReturnValue method is called to verify the return value.

Summary and comparison of SpringMVC and AOP verification mechanism

  1. SpringMVC only takes effect if there are @Valid , @Validated before the method enters the parameter object, or the custom name starts with "Valid"; AOP needs to mark @Validated
    , Then mark the verification rule comment before entering the method (eg: @NotBlank ), or mark @Valid before the verification object.
  2. SpringMVC does parameter verification in the HandlerMethodArgumentResolver implementation class, so it can only be validated at the Controller layer, and only part of the HandlerMethodArgumentResolver implementation class has a verification function (such as RequestParamMethodArgumentResolver does not); AOP is Spring's proxy mechanism, so only Spring proxy Bean
    You can do the verification.
  3. HandlerMethodArgumentResolver parameters of custom objects, but cannot verify the return value (now 060f7b869922e5 provided by Spring does not do this function, you can implement it by implementing the Resolver yourself); AOP can verify basic data types, and you can verify Check the return value.
  4. BindException exception when the verification fails MethodArgumentNotValidException has also become a BindException of 060f7b8699230f in the Spring5.3 version); AOP
    When the verification fails, the ConstraintViolationException exception will be thrown. (Tip: So you can determine which verification process to follow by throwing exceptions, which is convenient for locating problems).
  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: SptingBoot parameter verification mechanism in detail, using verification will no longer be confused

阅读 3.5k

Playing and Coding

916 声望
377 粉丝
0 条评论

Playing and Coding

916 声望
377 粉丝
文章目录
宣传栏