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