最近在落地 DDD,希望对 command 进行参数校验,由于部分流量入口是 MQ,所以希望在应用层是用 @Validated 进行参数校验,结果。。。
@Validated 注解的作用这里就不多做介绍了,具体用法在网上应该有不少。
在之前使用 MVC 架构编码时,通常是将 @Validated 注解或者 @Valid 配置在 Controller 的方法中,如下代码所示:
@PostMapping("common/set") public Response> setCommonSetting(@RequestBody @Validated SetCommonSettingReqVO reqVO) { //doSomeThings return Response.success(); } 复制代码
所以在配置应用层校验时,就想当然的按照类似的写法:
public void addClueTrack(@Validated AddClueTrackCommand command) { //doSomeThings } 复制代码
结果可想而知,@Validated 注解并不生效。
竟然不生效,那么就开始分析原因。
首先可以很容易想到,竟然能在方法执行前就拦截进行校验,那么大概率是使用动态代理。就和 @Transactional 事务注解一样,底层都是基于 AOP 实现动态代理。
接下来为了印证这个想法,就是需要深入看看 Spring 实现的。通过 IDE 可以很方便看到有哪些地方引用了 @Validated 注解:
其中一个类名一下就引起了我的注意 MethodValidationPostProcessor,熟悉 Spring 的小伙伴应该知道,Spring 中有很多 BeanPostProcessor 用于扩展 Bean,Aop 便是基于此实现动态代理的。点进去一看,果不其然:
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean { private Class extends Annotation> validatedAnnotationType = Validated.class; @Nullable private Validator validator; //... @Override public void afterPropertiesSet() { //创建切点 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()); } } public class AnnotationMatchingPointcut implements Pointcut { private final ClassFilter classFilter; private final MethodMatcher methodMatcher; public AnnotationMatchingPointcut(Class extends Annotation> classAnnotationType, boolean checkInherited) { //切点只针对类级别 this.classFilter = new AnnotationClassFilter(classAnnotationType, checkInherited); this.methodMatcher = MethodMatcher.TRUE; } //... } 复制代码
MethodValidationPostProcessor 中创建了一个切点,过滤类上添加了 @Validated 的 Bean,只要满足此条件,就会根据 MethodValidationInterceptor 生成对应的代理类。嗯,和 @Transactional 的实现原理差不多。
ok,看到这里我就在应用服务实现上添加了 @Validated 注解,那么此时注解生效了吗?哈哈,进度条还没过半呢😂
理论上类上加上 @Validated 注解,应该会生成动态代理类的,竟然没成功进行参数校验,我能想到的原因有二:
1. MethodValidationPostProcessor 没注入到 BeanFactory 中,所以没生成对应的代理类 2. MethodValidationInterceptor 对还有其他需要满足的条件,而目前还未满足
这里先剧透一下,答案是 2 🌝
竟然答案是2,那这里就先讲一下 MethodValidationInterceptor,MethodValidationPostProcessor 是怎么注册到容器的咱们后面再来讲。
ExecutableValidatorpublic class MethodValidationInterceptor implements MethodInterceptor { private final Validator validator; @Override @Nullable public Object invoke(MethodInvocation invocation) throws Throwable { // Standard Bean Validation 1.1 API ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation.getMethod(); Set> result; //获取类本身的实例(非代理类),请记住这里,这里就是和 Controller 最大的区别 Object target = invocation.getThis(); Assert.state(target != null, "Target must not be null"); try { //执行参数校验,校验的是当前类,也就是说校验的是 Bean 对应的类 result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { //doSomeThings } if (!result.isEmpty()) { throw new ConstraintViolationException(result); } //执行方法 Object returnValue = invocation.proceed(); //校验返回值 result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups); if (!result.isEmpty()) { throw new ConstraintViolationException(result); } return returnValue; } } 复制代码
接下来就要看看 ExecutableValidator.validateParameters 这个方法是如何实现的,为了方便阅读,这里我只保留了部分核心代码。根据包名我们大概也能猜到 ExecutableValidator.validateParameters 是 hibernate-validator 包提供的方法,而 @Validated 注解是由 Spring 提供的,所以不生效也就正常了。接下来我们继续往下走,我这里只贴部分核心的代码,中间的栈路径可以根据以下这个路径往下走:
/** * --> org.hibernate.validator.internal.engine.ValidatorImpl#validateParameters * --> org.hibernate.validator.internal.metadata.BeanMetaDataManager#getBeanMetaData * --> org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl#createBeanMetaData * --> org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl#getBeanConfigurationForHierarchy * --> org.hibernate.validator.internal.metadata.provider.MetaDataProvider#getBeanConfiguration * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#retrieveBeanConfiguration * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#getFieldMetaData * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#findPropertyMetaData * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#findConstraints * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#findCascadingMetaData * <-- ... * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#getMethodMetaData * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#getConstructorMetaData * --> org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider#getClassLevelConstraints * <-- ... * --> org.hibernate.validator.internal.metadata.aggregated.BeanMetaData#hasConstraints * --> org.hibernate.validator.internal.engine.ValidatorImpl#validateParametersInContext * */ public class ValidatorImpl implements Validator, ExecutableValidator { @Override public finalSet > validateValue(Class beanType, String propertyName, Object value, Class>... groups) { Contracts.assertNotNull( beanType, MESSAGES.beanTypeCannotBeNull() ); sanityCheckPropertyPath( propertyName ); sanityCheckGroups( groups ); //获取 bean 及其父类、超类的 BeanMetaData rootBeanMetaData = beanMetaDataManager.getBeanMetaData( beanType ); //判断该 bean 是否有约束 if ( !rootBeanMetaData.hasConstraints() ) { return Collections.emptySet(); } PathImpl propertyPath = PathImpl.createPathFromString( propertyName ); BaseBeanValidationContext validationContext = getValidationContextBuilder().forValidateValue( beanType, rootBeanMetaData, propertyPath ); ValidationOrder validationOrder = determineGroupValidationOrder( groups ); //校验参数 return validateValueInContext(validationContext, value, propertyPath, validationOrder); } //... } 复制代码
当我调试到 rootBeanMetaData.hasConstraints() 时,判断没有约束,然后就直接返回了没有进行参数校验。我就想说看看是如何判断 Bean 是否有约束的,于是就返回上层进入 beanMetaDataManager.getBeanMetaData 中看,结果发现里面的代码有够复杂的🌚
public class AnnotationMetaDataProvider implements MetaDataProvider { //获取类上所有的约束条件 privateBeanConfiguration retrieveBeanConfiguration(Class beanClass) { //获取字段上的约束条件 Set constrainedElements = getFieldMetaData( beanClass ); //获取方法上的约束条件(包括参数、返回值) constrainedElements.addAll( getMethodMetaData( beanClass ) ); //获取构造函数 constrainedElements.addAll( getConstructorMetaData( beanClass ) ); //获取类上的约束条件 Set > classLevelConstraints = getClassLevelConstraints( beanClass ); if ( !classLevelConstraints.isEmpty() ) { ConstrainedType classLevelMetaData = new ConstrainedType(ConfigurationSource.ANNOTATION, beanClass, classLevelConstraints); constrainedElements.add( classLevelMetaData ); } return new BeanConfiguration<>(ConfigurationSource.ANNOTATION, beanClass, constrainedElements, getDefaultGroupSequence( beanClass ), getDefaultGroupSequenceProvider( beanClass )); } //查找约束注解 protected List > findConstraintAnnotations(Constrainable constrainable, A annotation, ConstraintLocationKind type) { //如果包含 "jdk.internal" and "java" 下的注解,则直接不进行校验 if ( constraintCreationContext.getConstraintHelper().isJdkAnnotation( annotation.annotationType() ) ) { return Collections.emptyList(); } List constraints = newArrayList(); Class extends Annotation> annotationType = annotation.annotationType(); //判断是否有约束条件,也就我们经常配置的 @NotNull,@Min 这类注解 if ( constraintCreationContext.getConstraintHelper().isConstraintAnnotation( annotationType ) ) { constraints.add( annotation ); } //这个没用过,暂时跳过 else if ( constraintCreationContext.getConstraintHelper().isMultiValueConstraint( annotationType ) ) { constraints.addAll( constraintCreationContext.getConstraintHelper().getConstraintsFromMultiValueConstraint( annotation ) ); } return constraints.stream() .map( c -> buildConstraintDescriptor( constrainable, c, type ) ) .collect( Collectors.toList() ); } //构建级联元数据构造器,也就是我们常用的 @Valid,在 Bean 中如果我们要对对象属性进行校验, //需要在该属性上添加 @Valid,此处便是如此 private CascadingMetaDataBuilder getCascadingMetaData(JavaBeanAnnotatedElement annotatedElement, Map , CascadingMetaDataBuilder> containerElementTypesCascadingMetaData) { return CascadingMetaDataBuilder.annotatedObject( annotatedElement.getType(), annotatedElement.isAnnotationPresent( Valid.class ), containerElementTypesCascadingMetaData, getGroupConversions( annotatedElement.getAnnotatedType() ) ); } } 复制代码
顺着上面的栈路径一直往下走,最终发现最核心的几个方法是 getFieldMetaData、getMethodMetaData、getConstructorMetaData、getClassLevelConstraints,这个几方法都是用于获取约束和级联元数据。那么里面到底是怎么获取约束元数据的呢,咱继续往里钻,可以看到最终调用了 findConstraintAnnotations 获取约束元数据,也就是我们平时用到的 @NotNull,@Min 等注解,通过 getCascadingMetaData 获取级联元数据,也就是 @Valid 注解。看到这,是不是很容易就能想到,知道我加上 @Valid 就能成功校验了呢?
于是我尝试了一波,果然没问题。嗯~ 长见识了😂。由于时间有限,ValidatorImpl.validateParametersInContext() 方法我就没有深入进去看了。感兴趣的小伙伴可以自行去看看!!🌝
明白了在应用服务实现,准确的说应该是普通 Bean 中应该怎么配置之 @Validated 和 @Valid 使其生效之后,我就很好奇为啥 Controller 只需要单独在方法上配置 @Validated 或者 @Valid 就能成功校验呢?
还记得上面通过 IDE 查看应用 @Validated 注解的类时,我们发现了 MethodValidationPostProcessor,还有另外几个类一看就很像 Controller 参数解析相关的类:
我在这几个类上各打了一个断点,最终进入的是 AbstractMessageConverterMethodArgumentResolver。
ok,那就看看他是怎么实现的,这里只贴了很参数校验相关的方法:
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver { protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { //获取分组信息 Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { //进行校验 binder.validate(validationHints); break; } } } } public abstract class ValidationAnnotationUtils { @Nullable 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); } if (annotationType.getSimpleName().startsWith("Valid")) { Object hints = AnnotationUtils.getValue(ann); return convertValidationHints(hints); } return null; } } public class DataBinder implements PropertyEditorRegistry, TypeConverter { public void validate(Object... validationHints) { //此处是关键所在,这里获取的是参数!!!和普通的 Bean 获取到的却是 Bean 本身 Object target = getTarget(); Assert.state(target != null, "No target to validate"); BindingResult bindingResult = getBindingResult(); // Call each validator with the same binding result for (Validator validator : getValidators()) { if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) { ((SmartValidator) validator).validate(target, bindingResult, validationHints); } else if (validator != null) { validator.validate(target, bindingResult); } } } } 复制代码
可以看到,对于 Controller 不论是直接在参数上加上 @Validated 或者 @Valid 注解,都会进入到校验方法,而且校验的就是参数!!!而 Bean 校验的却是 Bean 本身!!!
明白了 @Validated 的拦截实现的原理后,那么就只剩最后一个问题了,MethodValidationPostProcessor 和 AbstractMessageConverterMethodArgumentResolver 是怎么被注册到 BeanFactory 的。
其实不用看源码大概有也能猜到是 Spring Boot 自动装配的。为了印证一下,我还是贴一下源码:
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(ExecutableValidator.class) @ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider") @Import(PrimaryDefaultValidatorPostProcessor.class) public class ValidationAutoConfiguration { //... @Bean @ConditionalOnMissingBean public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, @Lazy Validator validator, ObjectProviderexcludeFilters) { FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(excludeFilters.orderedStream()); boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true); processor.setProxyTargetClass(proxyTargetClass); processor.setValidator(validator); return processor; } } 复制代码
另外就是 AbstractMessageConverterMethodArgumentResolver 的几个实现类,均由 RequestMappingHandlerAdapter 实例化,而 RequestMappingHandlerAdapter 大家知道有 WebMvcAutoConfiguration 自动装配,时间原因,这就不看了。
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { private ListgetDefaultArgumentResolvers() { List resolvers = new ArrayList<>(30); // Annotation-based argument resolution resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new PathVariableMethodArgumentResolver()); resolvers.add(new PathVariableMapMethodArgumentResolver()); resolvers.add(new MatrixVariableMethodArgumentResolver()); resolvers.add(new MatrixVariableMapMethodArgumentResolver()); resolvers.add(new ServletModelAttributeMethodProcessor(false)); resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new SessionAttributeMethodArgumentResolver()); resolvers.add(new RequestAttributeMethodArgumentResolver()); // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); resolvers.add(new ServletResponseMethodArgumentResolver()); resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RedirectAttributesMethodArgumentResolver()); resolvers.add(new ModelMethodProcessor()); resolvers.add(new MapMethodProcessor()); resolvers.add(new ErrorsMethodArgumentResolver()); resolvers.add(new SessionStatusMethodArgumentResolver()); resolvers.add(new UriComponentsBuilderMethodArgumentResolver()); if (KotlinDetector.isKotlinPresent()) { resolvers.add(new ContinuationHandlerMethodArgumentResolver()); } // Custom arguments if (getCustomArgumentResolvers() != null) { resolvers.addAll(getCustomArgumentResolvers()); } // Catch-all resolvers.add(new PrincipalMethodArgumentResolver()); resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true)); resolvers.add(new ServletModelAttributeMethodProcessor(true)); return resolvers; } } 复制代码
小结
1、在普通 Bean 中如果要通过注解的方式使用 hibernate-validator 进行校验的话,需要在类上添加 @Validated 注解,同时在方法上添加 @Valid 注解。或者也可以直接使用 @NotNull 等注解。
2、普通 Bean 使用 @Validated 是通过动态代理完成的。具体的拦截器便是他 MethodValidationInterceptor。
3、Controller 层之所以能 @Validated 和 @Valid 二选一,是因为校验的是参数本身,而普通 Bean 校验的是 Bean 本身。
4、至此,相信大家就不会没配置好 @Validated 导致失效了。