首页 > 其他分享 >【SpringBoot】@Valid @Validated 注解校验时机实现原理

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

时间:2024-09-25 11:51:08浏览次数:1  
标签:SpringBoot 校验 validator Valid test 注解 Validated public

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 以及类里的参数要有约束注解,有理解不对的地方还请指正哈。

标签:SpringBoot,校验,validator,Valid,test,注解,Validated,public
From: https://www.cnblogs.com/kukuxjx/p/18430454

相关文章

  • SpringBoot Email:搭建邮件发送服务指南?
    SpringBootEmail服务如何集成?怎么使用SpringBoot?SpringBootEmail提供了一个简单而强大的框架,使得在SpringBoot应用程序中集成邮件发送功能变得非常容易。AokSend将详细介绍如何使用SpringBootEmail搭建一个高效的邮件发送服务。SpringBootEmail:创建服务SpringBoo......
  • 【YashanDB知识库】YAS-04110 invalid variant name
    本文转自YashanDB官网,具体内容请见https://www.yashandb.com/newsinfo/7369202.html?templateId=1718516【标题】错误码处理【问题分类】查询语句报错【关键字】YAS-04110【问题描述】执行特定sql时,遇到相应报错【问题原因分析】字段中含有保留字,应使用双引号包裹字段名称【解决/规......
  • 基于Java+SpringBoot+Mysql明星资讯信息系统功能设计与实现七
    一、前言介绍:1.1项目摘要随着社会的不断进步和人们生活水平的提高,娱乐产业在全球范围内得到了迅猛发展。明星作为娱乐产业的重要组成部分,其资讯的获取和传播成为了广大观众和粉丝关注的焦点。因此,研究明星资讯的课题背景,可以深入了解娱乐产业的发展趋势和市场需求。互联......
  • 基于Java+SpringBoot+Mysql明星资讯信息系统功能设计与实现八
    一、前言介绍:1.1项目摘要随着社会的不断进步和人们生活水平的提高,娱乐产业在全球范围内得到了迅猛发展。明星作为娱乐产业的重要组成部分,其资讯的获取和传播成为了广大观众和粉丝关注的焦点。因此,研究明星资讯的课题背景,可以深入了解娱乐产业的发展趋势和市场需求。互联......
  • 【YashanDB知识库】YAS-04110 invalid variant name
    本文转自YashanDB官网,具体内容请见https://www.yashandb.com/newsinfo/7369202.html?templateId=1718516【标题】错误码处理【问题分类】查询语句报错【关键字】YAS-04110【问题描述】执行特定sql时,遇到相应报错【问题原因分析】字段中含有保留字,应使用双引号包裹字段名称【......
  • javaWeb项目-springboot+vue+mysql财务管理系统功能说明介绍
    项目源码资源(点击链接下载):java-springboot+vue财务管理系统源码(项目源码-说明文档)资源-CSDN文库项目关键技术: 1、java技术java页面实质上也是一个HTML页面,只不过它包含了用于产生动态网页内容的JAVA代码,这些JAVA代码可以是JAVABean、SQL语句、RMI对象等。例如一个java......
  • Springboot中动态管理定时任务
    引言基于cron表达式的定时任务实现,因为cron表达式对于每个任务不确定,所以使用线程池来动态的创建和销毁定时任务依赖因为使用的spring自带的调度功能,所以没有额外的依赖,我的项目版本为:使用首先需要定义一个线程池,使用@configuration注解配置importorg.springframework.co......
  • 基于SpringBoot和Vue的餐饮管理系统
      基于springboot+vue实现的餐饮管理系统 (源码+L文+ppt)4-078   第4章系统设计   4.1总体功能设计一般个人用户和管理者都需要登录才能进入餐饮管理系统,使用者登录时会在后台判断使用的权限类型,包括一般使用者和管理者,一般使用者只能对菜品信息提供查阅和......
  • 【JAVA开源】基于Vue和SpringBoot学科竞赛管理系统
    本文项目编号T047,文末自助获取源码\color{red}{T047,文末自助获取源码}......
  • 基于springboot在线点餐系统
     基于springboot+vue实现的点餐系统 (源码+L文+ppt)4-077    第4章系统设计   4.1总体功能设计一般个人用户和管理者都需要登录才能进入点餐系统,使用者登录时会在后台判断使用的权限类型,包括一般使用者和管理者,一般使用者只能对美食信息提供查阅和个别使用信......