关于springboot:详解SptingBoot参数校验机制使用校验不再混乱

44次阅读

共计 13237 个字符,预计需要花费 34 分钟才能阅读完成。

前言

Spring Validation 验证框架提供了十分便当的参数验证性能,只须要 @Validated 或者 @Valid 以及一些规定注解即可校验参数。

自己看网上很多 SpringBoot 参数校验教程以 “ 单个参数校验 ”“ 实体类参数校验 ” 这两个角度来分类(或者 ”Get 办法 ” 和 ”Post 办法 ” 分类,实际上也是一样的,甚至这种更容易让人产生误解)。
这种分类很容易让人感觉凌乱:注解 @Validated一会要标在类下面,一会又要标在参数前;异样又要解决 BindException,又要解决ConstraintViolationException
刚看的时候可能还记得住,过一段时间就容易记混了,特地是当两种形式同时在一个类里,就不记得到底怎么用,最初可能罗唆全副都加上 @Validated 注解了。

本文就从校验机制的角度进行分类,SpringBoot 的参数校验有两套机制,执行的时候会同时被两套机制管制。 两套机制除了管制各自的部份外,有局部是重叠的,这部分又会波及优先级之类的问题。然而只有晓得了两个机制是什么,且理解
Spring 流程,就再也不会搞混了。

校验机制

这两套校验机制,第一种由 SpringMVC 管制。这种校验只能在 ”Controller” 层应用,须要在被校验的对象前标注@Valid@Validated,或者自定义的名称以 ’Valid’ 结尾的注解,如:


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

另一种被 AOP 管制。这种只有是 Spring 治理的 Bean 就能失效,所以 ”Controller”,”Service”,”Dao” 层等都能够用这种参数校验。须要在被校验的类上标注 @Validated
注解,而后如果校验单个类型的参数,间接在参数前标注 @NotEmpty 之类的校验规定注解;如果校验对象,则在对象前标注 @Valid 注解(这里只能用@Valid,其余都无奈失效,起因前面阐明),如:


import javax.validation.constraints.Max;

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

SpringMVC 校验机制详解

首先大抵理解一下 SpringMVC 执行流程:

  1. 通过 DispatcherServlet 接管所有的前端发动的申请
  2. 通过配置获取对应的 HandlerMapping,将申请映射到处理器。即依据解析 url, http 协定,申请参数等找到对应的 Controller 的对应 Method 的信息。
  3. 通过配置获取对应的 HandlerAdapter,用于理论解决和调用 HandlerMapping。即实际上是 HandlerAdapter 调用到用户本人写的 Controller 的 Method。
  4. 通过配置获取对应的 ViewResolver,解决上一步调用获取的返回数据。

参数校验的性能是在步骤 3 做的,客户端申请个别通过 RequestMappingHandlerAdapter
一系列配置信息和封装,最终调用到 ServletInvocableHandlerMethod.invokeHandlerMethod()
办法。

HandlerMethod

这个 ServletInvocableHandlerMethod 继承了 InvocableHandlerMethod,作用就是负责调用HandlerMethod
HandlerMethod 是 SpringMVC 中十分重要的一个类,大家最常接触的中央就是在拦截器 HandlerInterceptor 中的第三个入参 Object handler,尽管这个入参是Object
类型的,但通常都会强转成HandlerMethod。它用于封装“Controller”,简直所有在调用时可能用到的信息,如办法、办法参数、办法上的注解、所属类,都会被提前解决好放到这个类里。

HandlerMethod自身只封装存储数据,不提供具体的应用办法,所以 InvocableHandlerMethod 就呈现了,它负责去执行 HandlerMethod
,而ServletInvocableHandlerMethod 在其根底上减少了返回值和响应状态码的解决。

这里贴一下源码作者对这两个类的正文:

InvocableHandlerMethod调用 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);
}

第一行 getMethodArgumentValues() 就是把申请参数映射到 Java 对象的办法,来看看这个办法:

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

办法里最次要的就是 this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); 这行
,调用HandlerMethodArgumentResolver 接口的实现类解决参数。

HandlerMethodArgumentResolver

HandlerMethodArgumentResolver也是 SpringMVC 中十分重要的一个组件局部,用于将办法参数解析为参数值的策略接口,咱们常说的自定义参数解析器。接口有两个办法:
supportsParameter办法用户断定该 MethodParameter 是否由这个 Resolver 解决,resolveArgument办法用于解析参数成办法的入参对象。

public interface HandlerMethodArgumentResolver {boolean supportsParameter(MethodParameter parameter);

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

SpringMVC 本身提供了十分多的 HandlerMethodArgumentResolver 实现类,如 RequestResponseBodyMethodProcessor@RequestBody注解的参数)
RequestParamMethodArgumentResolver@RequestParam注解的参数,或者没其余 Resolver 匹配的 Java 根本数据类型)
RequestHeaderMethodArgumentResolver@RequestHeaderMethodArgumentResolver注解的参数)
ServletModelAttributeMethodProcessor@ModelAttribute注解的参数,或者没其余 Resolver 匹配的自定义对象)等等。

咱们以 ServletModelAttributeMethodProcessor 为例,看看其 resolveArgument 是怎么样的:

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

在代码中步骤 4 调用 validateIfApplicable 办法看名字就是校验的,看看代码:

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)办法用于断定这个参数对象是否有满足参数校验条件的正文,并且返回对应的分组信息(@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;
}

这里就是结尾说的『这种校验只能在 ”Controller” 层应用,须要在被校验的对象前标注 @Valid@Validated,或者自定义的名称以 ’Valid’ 结尾的注解』的 SpringMVC 断定是否要做校验的代码。
如果是 @Validated 则返回 @Validated 里的分组数据,否则返回空数据,如果没有符合条件的注解,则返回 null。

断定完校验条件,接着 binder.validate(validationHints); 会调用到 SmartValidator
解决分组信息,最终调用到 org.hibernate.validator.internal.engine.ValidatorImpl.validateValue 办法去做理论的校验逻辑。

总结一下:

SpringMVC 的校验是在 HandlerMethodArgumentResolver 的实现类中,resolveArgument 办法实现的代码中编写相应的校验规定,是否校验的断定是由 ValidationAnnotationUtils.determineValidationHints(ann) 来决定。

然而只有 ModelAttributeMethodProcessorAbstractMessageConverterMethodArgumentResolver 这两个抽象类的 resolveArgument 办法编写了校验逻辑,实现类别离为:

ServletModelAttributeMethodProcessor(@ModelAttribute注解的参数,或者没其余 Resolver 匹配的自定义对象),

HttpEntityMethodProcessor(HttpEntityRequestEntity 对象),

RequestPartMethodArgumentResolver(@RequestPart注解的参数或 MultipartFile 类),RequestResponseBodyMethodProcessor(@RequestBody注解的对象)

开发中常常应用的 @RequestParam 注解的参数或者说单个参数的 Resolver 并没有实现校验逻辑,然而这部分在应用中也能被校验,那是因为这部分校验是交给 AOP 机制的校验规定解决的

AOP 校验机制详解

在下面『SpringMVC 校验机制详解』局部提到在 DispatcherServlet 的流程中,会有 InvocableHandlerMethod 调用 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);
}

这个 getMethodArgumentValues 办法在下面剖析了,会获取到 request 中的参数并校验组装成 Method 须要的参数,这一节看看 doInvoke(args) 办法做了什么。

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获取到 HandlerMethod 里的 Method 和 Bean 对象,而后通过 java 原生反射性能调用到咱们编写的 Controller 里的业务代码。

MethodValidationInterceptor

既然这里获取的是 Spring 治理的 Bean 对象,那么必定是被 ” 代理 ” 过的,要代理必定就要有切点切面,那就看看 @Validated 注解被什么类调用过。发现有个名叫 MethodValidationInterceptor
的类调用到了,这名字一看就和校验性能无关,且是个拦截器,看看这个类的正文。

正文写的很间接,第一句就说这是 AOP 的 MethodInterceptor 的实现类,提供了办法级的校验性能。

MethodValidationInterceptor算是 AOP 机制中的告诉(Advice)局部,由 MethodValidationPostProcessor 类注册到 Spring 的 AOP 治理中:

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初始化 Bean
的时候,留神 Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); 这行代码,
创立了一个 AnnotationMatchingPointcut 的切点类,会把类上有 Validated 注解的做 AOP 代理。

所以 AOP 机制做校验的首要条件就是类上要有 Validated 注解。所以只有是被 Spring 治理的 Bean 就能够用 AOP 机制做参数校验

当初来看一下 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]);
    }
}

这里 invoke 代理办法次要做了几个步骤:

  1. 调用 determineValidationGroups 办法获取 Validated 里的 Group 分组信息。优先查找办法上的 Validated 注解来获取分组信息,如果没有则用类上的 Validated 注解的分组信息。
  2. 获取校验器类,通常为ValidatorImpl
  3. 调用校验办法 ExecutableValidator.validateParameters 校验入参,如果抛出 IllegalArgumentException
    异样,尝试获取其泛型信息再次校验。如果参数校验不通过会抛出 ConstraintViolationException 异样
  4. 调用校验办法 ExecutableValidator.validateReturnValue 校验返回值。如果参数校验不通过会抛出 ConstraintViolationException 异样

总结一下:
SpringMVC 会通过反射调用到 Controller 对应的业务代码,被调用的类就是被 Spring AOP 代理的类,会走 AOP 机制。
校验性能是在 MethodValidationInterceptor 类中调用的,调用 ExecutableValidator.validateParameters 办法校验入参,调用 ExecutableValidator.validateReturnValue 办法校验返回值

SpringMVC 和 AOP 校验机制总结与比对

  1. SpringMVC 只有办法入参对象前有@Valid@Validated,或者自定义的名称以 ’Valid’ 结尾的注解才失效;AOP 须要先在类上标注@Validated
    , 而后办法入参前标注校验规定注解(如:@NotBlank),或者校验对象前标注@Valid
  2. SpringMVC 在 HandlerMethodArgumentResolver 实现类中做参数校验,所以只能在 Controller 层校验失效,并且只有局部 HandlerMethodArgumentResolver 实现类有校验性能(如 RequestParamMethodArgumentResolver 就没有);AOP 是 Spring 的代理机制,所以只有 Spring 代理的 Bean
    即可做校验。
  3. 目前 SpringMVC 校验只能校验自定义对象的入参,无奈校验返回值(当初 Spring 提供的 HandlerMethodArgumentResolver 没有做这个性能,能够通过本人实现 Resolver 来实现);AOP 能够校验根本数据类型,能够校验返回值。
  4. SpringMVC 在校验不通过时会抛出 BindException 异样(MethodArgumentNotValidException在 Spring5.3 版本也变为 BindException 的子类);AOP
    校验在校验不通过时抛出 ConstraintViolationException 异样。(Tip: 所以能够通过抛出的异样来断定走的哪个校验流程,不便定位问题)。
  5. 在 Controller 层校验时会先走 SpringMVC 流程,而后再走 AOP 校验流程。

原文地址:详解 SptingBoot 参数校验机制,应用校验不再凌乱

正文完
 0