【SpringBoot】@Validated @Valid 注解校验时机实现原理

1  前言

上节我们看了【SpringBoot】@Validated @Valid 参数校验概述以及使用方式,对于 @Valid 以及 @Validated 有了大概的认识,并也尝试了集中校验方式,那么本节我们重点看一下 SpringBoot 中 @Valid @Validated 的校验实现原理。

2  准备工作

客户类我还是用上节的那个类,然后我们这里新建个 Controller :

复制代码
@Data
public class Customer {

    /**
     * 客户编码
     */
    @NotBlank
    private String code;

    /**
     * 客户名称
     */
    @NotBlank
    private String name;

}
复制代码
复制代码
@RestController
@RequestMapping("/validator")
public class ValidatorController {

    @PostMapping("/test")
    public void test(@Valid @RequestBody Customer customer) {
        System.out.println("test");
    }
}
复制代码

我提个问题,看上边的我用的是 @Valid 注解(javax包里的注解),你说它会校验么?

答案是会的,我们这是在 SpringBoot 体系下了对不对, 而我们用的是 @Valid 注解(javax包里的注解),SpringBoot 应该不会去解析这个注解吧,按我的理解它应该只会识别 @Validated ,其实他俩都会自动校验,只是作用的点不太一样或者说是触发的方式时机有区别,我们下边就来看看。

3  实现原理

校验触发的时机,其实是从两个点触发,一个跟 SpringMVC 的请求处理过程息息相关,一个是跟 MethodValidationPostProcessor 相关,我们接下来就主要看下这两种时机的执行原理或者触发时机。

3.1  SpringMVC 请求处理过程触发校验时机

一谈到 SpringMVC 要知道一个核心类就是 DispatcherServlet,它的处理过程大致是:

(1)请求到达:

当客户端发送HTTP请求时,请求首先到达Web服务器(如Tomcat),然后由Web服务器转发给DispatcherServlet。

(2)初始化请求:

DispatcherServlet接收到请求后,首先会对请求进行一些预处理,如设置字符编码等。

(3)查找HandlerMapping:

DispatcherServlet会使用HandlerMapping接口的实现来查找匹配的控制器(Controller)。

HandlerMapping根据请求URL和其他信息找到合适的控制器方法,并返回一个HandlerExecutionChain对象,其中包含控制器方法和拦截器。

(4)创建HandlerAdapter:

找到控制器后,DispatcherServlet会创建一个HandlerAdapter实例来处理请求。

HandlerAdapter负责调用控制器方法,并处理其返回值。

(5)解析方法参数:

在调用控制器方法之前,HandlerAdapter会使用HandlerMethodArgumentResolver接口的实现来解析方法参数。

这些解析器会根据参数类型和注解来填充方法参数,如从请求体中读取JSON数据、从查询字符串中读取参数等。

(6)验证参数:

如果方法参数上有@Valid或@Validated注解,则会触发验证逻辑。

ValidatingMethodArgumentResolver会使用验证框架(如Hibernate Validator)来验证这些参数,并在验证失败时抛出MethodArgumentNotValidException异常。

(7)执行控制器方法:

参数验证通过后,HandlerAdapter会调用控制器方法。

控制器方法执行完毕后,返回一个ModelAndView对象,包含视图名和模型数据。

(8)处理返回结果:

HandlerAdapter会处理控制器方法的返回结果,根据返回值类型来决定下一步操作。

可能的操作包括渲染视图、返回JSON响应等。

(9)渲染视图:

最终,DispatcherServlet会根据ModelAndView对象中的视图名来选择一个视图(View)。

视图会使用模型数据来生成最终的响应HTML页面或其他格式的内容。

(10)响应客户端:

渲染完成后的内容会被写回到HTTP响应中,返回给客户端。

SpringMVC 的具体源码处理过程,我这里就不一点点看了哈,我直接画了个图(唉,现在导出居然带水印了),大家看一下:

这还只是看了一下主要的节点,看不清的话,大家可以保存到电脑上打开,我试过是清晰的。红色五角星的地方就是我们本节的一个触发点,它是在 AbstractMessageConverterMethodArgumentResolver 的 validateIfApplicable 方法:

复制代码
// AbstractMessageConverterMethodArgumentResolver
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    Annotation[] annotations = parameter.getParameterAnnotations();
    for (Annotation ann : annotations) {
        // 首先判断一下是不是 @Validated 注解 
        Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
        // 存在 @Validated 注解 或者注解的类型名称是以 Valid 开头的都会开始走校验
        if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
            Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
            Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
            binder.validate(validationHints);
            break;
        }
    }
}
复制代码

3.2  MethodValidationPostProcessor AOP增强方式触发校验时机

AOP 方式的话其实就比较好理解了,通过创建代理的方式,进入增强逻辑继而进行参数的校验。这个主要的类我们上节也看了,首先是 ValidationAutoConfiguration 引入 MethodValidationPostProcessor 处理器,我们这里主要看下这个类:

复制代码
// MethodValidationPostProcessor
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
        implements InitializingBean {

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

    @Nullable
    private Validator validator;


    //
    public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {
        Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
        this.validatedAnnotationType = validatedAnnotationType;
    }

    /**
     * 设置 Validator
     */
    public void setValidator(Validator validator) {
        // Unwrap to the native Validator with forExecutables support
        if (validator instanceof LocalValidatorFactoryBean) {
            this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
        }
        else if (validator instanceof SpringValidatorAdapter) {
            this.validator = validator.unwrap(Validator.class);
        }
        else {
            this.validator = validator;
        }
    }

    public void setValidatorFactory(ValidatorFactory validatorFactory) {
        this.validator = validatorFactory.getValidator();
    }


    @Override
    public void afterPropertiesSet() {
        // 创建解析 @Validated 注解的切点
        Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
        // 新增 advisor
        this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
    }

    /**
     * 增强逻辑处理 MethodValidationInterceptor
     */
    protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
        return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
    }

}
复制代码

那我们就再看一下 MethodValidationInterceptor 的 invoke 方法:

复制代码
// MethodValidationInterceptor
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
    // 筛选Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
    if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
        return invocation.proceed();
    }
    // 校验组
    Class<?>[] groups = determineValidationGroups(invocation);
    // 获取校验工具 Standard Bean Validation 1.1 API
    ExecutableValidator execVal = this.validator.forExecutables();
    Method methodToValidate = invocation.getMethod();
    Set<ConstraintViolation<Object>> result;
    try {
        // 执行参数校验
        result = execVal.validateParameters(
                invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
    }
    catch (IllegalArgumentException ex) {
        // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
        // Let's try to find the bridged method on the implementation class...
        methodToValidate = BridgeMethodResolver.findBridgedMethod(
                ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
        result = execVal.validateParameters(
                invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
    }
    if (!result.isEmpty()) {
        throw new ConstraintViolationException(result);
    }
    // 原方法执行
    Object returnValue = invocation.proceed();
    // 执行结果校验
    result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
    if (!result.isEmpty()) {
        throw new ConstraintViolationException(result);
    }
    return returnValue;
}
复制代码

并且两个触发点最后的落点其实就是获取到当前上下文中的 Validator,然后进行校验。

4  疑问

看完上边两个触发时机,不知道你有没有疑问,比如我们上边的这个例子,SpringMVC 里参数绑定的时候会校验一次,MethodValidationPostProcessor 的增强是不是又会校验一次,那岂不是要校验两次呢,重复校验?

复制代码
@RestController
@RequestMapping("/validator")
public class ValidatorController {

    @PostMapping("/test")
    public void test(@Validated @RequestBody Customer customer) {
        System.out.println("test");
    }
}
复制代码

这个我测试了一下,首先将请求参数补全:

{
    "code":"1",
    "name":"客户"
}

可以通过 SpringMVC 的校验,然后我在 MethodValidationInterceptor 的 invoke 增强处打了一个断点,发现它并没有进入断点,说明没有进入增强。

也说明 MethodValidationPostProcessor 里的这个 advisor 并没有给我的 controller 创建代理是吧。

@Override
public void afterPropertiesSet() {
    Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
    this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}

然后我在服务启动判断是否创建代理的地方,即它的父类 AbstractBeanFactoryAwareAdvisingPostProcessor 里的 isEligible 方法打个断点,继而进入 AopUtils 的 canApply 方法:

advisor 就是上边的 new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));

targetClass 就是我的 controller

复制代码
public static boolean canApply(Advisor advisor, Class<?> targetClass) {
    return canApply(advisor, targetClass, false);
}
// element 是我们的 controller  annotationType 是 @Validated
public static boolean hasAnnotation(AnnotatedElement element, Class<? extends Annotation> annotationType) {
    // Shortcut: directly present on the element, with no merging needed?
    if (AnnotationFilter.PLAIN.matches(annotationType) ||
            AnnotationsScanner.hasPlainJavaAnnotationsOnly(element)) {
        return element.isAnnotationPresent(annotationType);
    }
    // Exhaustive retrieval of merged annotations...
    return findAnnotations(element).isPresent(annotationType);
}
复制代码

但是你看这个 targetClass 它已经是个代理对象了,代理对象即它的代理类里没有注解信息的,所以就不会再创建代理对象,也就不会进入增强了。

那我再单独的写一个 Component:

@Component
public class ValidatorUtil {
    public void test(@Validated Customer customer) {
        System.out.println("test");
    }
}

然后在 controller 里试一下,发现还是没有进入增强。

复制代码
@RestController
@RequestMapping("/validator")
public class ValidatorController {

    @Autowired
    private ValidatorUtil validatorUtil;

    @PostMapping("/test")
    public void test(@Validated @RequestBody Customer customer) {
        validatorUtil.test(customer);
        System.out.println("test");
    }
}
复制代码

最后在创建代理的过程中,发现:

复制代码
@Nullable
private <C, R> R scan(C criteria, AnnotationsProcessor<C, R> processor) {
    if (this.annotations != null) {
        R result = processor.doWithAnnotations(criteria, 0, this.source, this.annotations);
        return processor.finish(result);
    }
    if (this.element != null && this.searchStrategy != null) {
        // element 是我们的类 扫描类里的注解
        return AnnotationsScanner.scan(criteria, this.element, this.searchStrategy, processor);
    }
    return null;
}
// source 是类的话 processClass 只会获取类上(父类父接口等)的注解,并不会去判断某个方法或者方法参数里的注解
@Nullable
private static <C, R> R process(C context, AnnotatedElement source,
        SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor) {
    if (source instanceof Class) {
        return processClass(context, (Class<?>) source, searchStrategy, processor);
    }
    if (source instanceof Method) {
        return processMethod(context, (Method) source, searchStrategy, processor);
    }
    return processElement(context, source, processor);
}
复制代码

所以当我们的 Controller 类上有 @Validated 的时候,才会进入增强,我们试试,确实可以进入增强,但是约束为空,不校验我们的参数。

复制代码
// 当存在某个约束比如我加个 @NotNull 才会开始校验参数。
@Validated
@RestController
@RequestMapping("/validator")
public class ValidatorController {

    @PostMapping("/test")
    public void test(@NotNull @RequestBody Customer customer) {
        System.out.println("test");
    }
}
复制代码

所以要想 MethodValidationPostProcessor 发挥作用,我们的类上要有 @Validated 标识,并且类中的属性或者方法的参数要有约束注解,才会起作用。

5  小结

好啦,本节我们主要看了下参数校验时机的两种入口或者方式,一种是依托于 SpringMVC 一种是通过 AOP 增强,并看了下 AOP 增强生效的关键是类上要有 @Validated 以及类里的参数要有约束注解,有理解不对的地方还请指正哈。

posted @   酷酷-  阅读(712)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示