最近在落地 DDD,希望对 command 进行参数校验,因为部分流量入口是 MQ,所以希望在使用层是用 @Validated 进行参数校验,成果。。。

Controller 中使用 @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 注解并不收效。

@Validated 是怎样收效的?

居然不收效,那么就开始剖析原因。

首先能够很容易想到,居然能在办法履行前就阻拦进行校验,那么大约率是使用动态署理。就和 @Transactional 业务注解一样,底层都是基于 AOP 完成动态署理。

接下来为了印证这个想法,便是需求深化看看 Spring 完成的。经过 IDE 能够很便利看到有哪些地方引用了 @Validated 注解:

Spring @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

MethodValidationInterceptor 需求满足什么条件

居然答案是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<ConstraintViolation<Object>> 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 final <T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType, String propertyName, Object value, Class<?>... groups) {
        Contracts.assertNotNull( beanType, MESSAGES.beanTypeCannotBeNull() );
        sanityCheckPropertyPath( propertyName );
        sanityCheckGroups( groups );
        //获取 bean 及其父类、超类的
        BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( beanType );
        //判别该 bean 是否有束缚
        if ( !rootBeanMetaData.hasConstraints() ) {
            return Collections.emptySet();
        }
        PathImpl propertyPath = PathImpl.createPathFromString( propertyName );
        BaseBeanValidationContext<T> 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 {
    //获取类上一切的束缚条件
    private <T> BeanConfiguration<T> retrieveBeanConfiguration(Class<T> beanClass) {
        //获取字段上的束缚条件
        Set<ConstrainedElement> constrainedElements = getFieldMetaData( beanClass );
        //获取办法上的束缚条件(包含参数、回来值)
        constrainedElements.addAll( getMethodMetaData( beanClass ) );
        //获取构造函数
        constrainedElements.addAll( getConstructorMetaData( beanClass ) );
        //获取类上的束缚条件
        Set<MetaConstraint<?>> 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 <A extends Annotation> List<ConstraintDescriptorImpl<?>> findConstraintAnnotations(Constrainable constrainable, A annotation, ConstraintLocationKind type) {
      //如果包含 "jdk.internal" and "java" 下的注解,则直接不进行校验
          if ( constraintCreationContext.getConstraintHelper().isJdkAnnotation( annotation.annotationType() ) ) {
          return Collections.emptyList();
      }
      List<Annotation> 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<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData) {
        return CascadingMetaDataBuilder.annotatedObject( annotatedElement.getType(), annotatedElement.isAnnotationPresent( Valid.class ),
            containerElementTypesCascadingMetaData, getGroupConversions( annotatedElement.getAnnotatedType() ) );
    }
}

顺着上面的栈途径一向往下走,终究发现最中心的几个办法是 getFieldMetaDatagetMethodMetaDatagetConstructorMetaDatagetClassLevelConstraints,这个几办法都是用于获取束缚和级联元数据。那么里边到底是怎样获取束缚元数据的呢,咱持续往里钻,能够看到终究调用了 findConstraintAnnotations 获取束缚元数据,也便是咱们平常用到的 @NotNull,@Min 等注解,经过 getCascadingMetaData 获取级联元数据,也便是 @Valid 注解。看到这,是不是很容易就能想到,知道我加上 @Valid 就能成功校验了呢?

所以我尝试了一波,果然没问题。嗯~ 长见识了。因为时刻有限,ValidatorImpl.validateParametersInContext() 办法我就没有深化进去看了。感兴趣的小伙伴能够自行去看看!!

那么 Controller 为啥直接增加 @Validated 或许 @Valid 就能够呢?

理解了在使用服务完成,准确的说应该是一般 Bean 中应该怎样装备之 @Validated 和 @Valid 使其收效之后,我就很猎奇为啥 Controller 只需求单独在办法上装备 @Validated 或许 @Valid 就能成功校验呢?

还记得上面经过 IDE 检查使用 @Validated 注解的类时,咱们发现了 MethodValidationPostProcessor,还有别的几个类一看就很像 Controller 参数解析相关的类:

Spring @Validated 失效分析

我在这几个类上各打了一个断点,终究进入的是 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 自身!!!

MethodValidationPostProcessor 和 AbstractMessageConverterMethodArgumentResolver 是怎样被注册到 BeanFactory 的?

理解了 @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, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) {
        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 主动安装,时刻原因,这就不看了。

Spring @Validated 失效分析

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
        implements BeanFactoryAware, InitializingBean {
    private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
        List<HandlerMethodArgumentResolver> 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 导致失效了。