@Validated 注解失效引发的探索

291 阅读7分钟

@Validated 和 @Valid 校验失败的案例

失败案例

public interface SpecialService {
    @Validated(value = {SpecialInfoValidationGroups.AddSpecialInfo.class})
    Long addSpecialInfo(@Valid @NotNull(message = "保存专项信息入参不能为空")SpecialInfoSaveParam specialInfoSaveParam);
}
@Service
public class SpecialServiceImpl implements SpecialService {

    @Override
    public Long addSpecialInfo(SpecialInfoSaveParam specialInfoSaveParam) {
    // 实现类没啥特别的
}

失败原因

  • 需要在 SpecialService 这个接口上加 @Validated 注解,即
@Validated 
public interface SpecialService {
    @Validated(value = {SpecialInfoValidationGroups.AddSpecialInfo.class})
    Long addSpecialInfo(@Valid @NotNull(message = "保存专项信息入参不能为空")SpecialInfoSaveParam specialInfoSaveParam);
}

注解及使用说明

@Valid

  • @Valid: 是一个 JSR-303(Bean Validation 1.0)标准注解,主要用于验证方法参数、返回值(注意返回值也是可以校验的哦)或对象的属性是否符合预期。它会根据 Bean Validation 规则对被注解的对象进行验证。

@Validated

  • @Validated: 是 Spring Framework 提供的扩展注解,用于在 Spring MVC 控制器或 Spring Service 层中开启方法级别的 Bean 验证。它可以与 Spring 的 @Controller、@RestController、@Service 等注解一起使用。

区分点

  • 分组验证:

    • @Validated 提供了分组功能,允许在不同的场景下对同一个对象应用不同的验证规则
    • @Valid 不支持分组验证
  • 注解位置:

    • @Validated 可以用在类、方法和方法参数上,但不能用于成员属性(字段)上
    • @Valid 可以用在方法、构造函数、方法参数和成员属性(字段)上
  • 嵌套验证:

    • @Validated 在方法参数上使用时,不能单独提供嵌套验证功能,需要与 @Valid 结合使用以实现嵌套验证
    • @Valid 用在方法参数上时,也不能单独提供嵌套验证功能,但它可以用在成员属性(字段)上,提示验证框架进行嵌套验证

组合使用

为什么要在 SpecialService 接口上使用 @Validated 并且方法参数上同时使用 @Valid 和 @Validated ?

  • @Validated 在接口上使用是为了启用整个接口级别的验证,确保所有实现该接口的方法都会进行参数验证

  • 方法参数上的 @Valid 确保了对单个参数对象的验证,包括其嵌套对象的验证。如果没有 @Valid,则嵌套对象不会被验证

  • @Validated 在方法参数上使用是为了应用分组验证,这里通过指定 SpecialInfoValidationGroups.AddSpecialInfo.class 来应用特定的验证规则

更多使用方式

更多详细的用法可以参考

疑惑

为何要在类上加 @Validated 注解

为何要组合使用?

MethodValidationInterceptor 何时执行的?

Valid 为何可以实现嵌套对象校验

带着这些疑问我们一起来看下参数校验的实现源码

源码探索

ValidationAutoConfiguration

想要理解 @Validated 和 @Valid 注解是怎么实现校验的,需要先找到校验入口

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({ExecutableValidator.class})
@ConditionalOnResource(
    resources = {"classpath:META-INF/services/javax.validation.spi.ValidationProvider"}
)
@Import({PrimaryDefaultValidatorPostProcessor.class})
public class ValidationAutoConfiguration {
    public ValidationAutoConfiguration() {
    }

    // 注册 LocalValidatorFactoryBean,提供 Validator 实例
    @Bean
    @Role(2)
    @ConditionalOnMissingBean({Validator.class})
    public static LocalValidatorFactoryBean defaultValidator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
        factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
        return factoryBean;
    }

    // 注入 MethodValidationPostProcessor 
    @Bean
    @ConditionalOnMissingBean
    public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, @Lazy Validator validator) {
        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
        boolean proxyTargetClass = (Boolean)environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
        processor.setProxyTargetClass(proxyTargetClass);
        processor.setValidator(validator);
        return processor;
    }
}

LocalValidatorFactoryBean

LocalValidatorFactoryBean 被注册到 Spring 容器中,其他组件可以通过注入Validator 接口来使用这个校验器实例,执行具体的校验逻辑

public class LocalValidatorFactoryBean extends SpringValidatorAdapter
		implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean {

	@Override
	@SuppressWarnings({"rawtypes", "unchecked"})
	public void afterPropertiesSet() {
		// ...

        // traversableResolver 用于决定哪些对象的属性需要进行递归验证。如果提供了自定义的 traversableResolver,则在这里设置
		if (this.traversableResolver != null) {
			configuration.traversableResolver(this.traversableResolver);
		}

		// ...

		this.validatorFactory = configuration.buildValidatorFactory();
		setTargetValidator(this.validatorFactory.getValidator());
	}
}

MethodValidationPostProcessor

  • org.springframework.validation.beanvalidation.MethodValidationPostProcessor image.png

通过类名显然可以发现这是一个后置处理器,在 Bean 初始化完成后对 Bean 进行一些操作。

  • 这个后置处理器的具体作用:

    • 是 Spring 框架中用于处理方法级别的验证的后置处理器,在 Spring 容器初始化完成后,对所有 Bean 的方法进行增强,以便在方法执行前进行参数验证
    • 会查找所有使用了 @Validated 注解的方法,并为这些方法添加一个MethodValidationInterceptor(这个后面会说)。
  • 源码分析-setValidatorFactory 方法

public void setValidatorFactory(ValidatorFactory validatorFactory) {
    // 这里赋值的就是 LocalValidatorFactoryBean 创建的 Validator 实例
    this.validator = validatorFactory.getValidator();
}
  • 源码分析-afterPropertiesSet 方法
    • InitializingBean 接口的方法,在 Spring 容器设置完所有属性后调用的,它配置了AOP的切点(Pointcut)和增强(Advisor)
    • AnnotationMatchingPointcut 是关键,它用于匹配所有被validatedAnnotationType 指定的注解标记的方法
@Override
public void afterPropertiesSet() {
    Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
    this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
  • 源码分析-AnnotationMatchingPointcut

// 匹配所有类或方法上带有 validatedAnnotationType 注解的方法。第二个参数 true 表示匹配类上的注解,以及类中的方法。
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);

// AnnotationClassFilter 类中的 matches 方法可以说明: 传入 true 表示匹配类上的注解,以及类中的方法
  • 源码分析-createMethodValidationAdvice 方法
// 创建了一个 Advice 对象,实际上是 MethodValidationInterceptor 的一个实例,它包含了实际的验证逻辑
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
		return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
	}

MethodValidationInterceptor

public Object invoke(MethodInvocation invocation) throws Throwable {
		if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
			return invocation.proceed();
		}
        // 调用 determineValidationGroups 方法来确定当前方法调用应该使用哪些校验组(groups)
		Class<?>[] groups = determineValidationGroups(invocation);


		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) {
			methodToValidate = BridgeMethodResolver.findBridgedMethod(
					ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
	        // 校验方法的入参如果发生 IllegalArgumentException 异常,可能是因为接口和实现类之间的泛型类型不匹配,这时会尝试找到实现类上的桥接方法,并重新进行参数校验
			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;
}

解惑

在类上加 @Validated 注解

  • 如果接口上没有 @Validated 注解,那么 MethodValidationPostProcessor 不会自动为实现这个接口的类的方法添加 MethodValidationInterceptor 。因此即使在方法参数上使用了 @Valid或@Validated注解,但没有 MethodValidationPostProcessor 的增强,这些注解不会触发任何验证逻辑

要组合使用

注意这个组合使用仅使用如下情况: 当我们要对普通方法上的 JavaBean 参数进行校验必须满足下面两个条件:①方法所在的类上添加 @Vlidated、②待校验的 JavaBean 参数上添加 @Valid

  • 从网上查询以及 ChatGPT 询问得到的是答案是: 更底层的ExecutableValidator 等相关校验逻辑实现代码里完成了第二个条件的识别,即 result = execVal.validateParameters 方法会校验方法参数是否加上了 @Valid 参数

  • 但是从该方法的实现上看,没有直接发现对第二个条件进行的识别,有发现的同学可以贴出代码一起学习下!

如何触发执行校验

MethodValidationPostProcessor 和 MethodValidationInterceptor 都没有加 @Component 注解,是如何执行的呢?

  • 这就回到开头提到的入口: ValidationAutoConfiguration,这个类自动装配了俩个 Bean,其中就包括: MethodValidationPostProcessor

  • MethodValidationInterceptor 通常是在 MethodValidationPostProcessor 内部使用的。它不是直接通过 @Component 被 Spring 容器加载。MethodValidationPostProcessor 在创建代理对象时,会将 MethodValidationInterceptor 添加到代理对象的拦截器链中

嵌套验证对象

  • TraversableResolver 是一个接口,用于在验证过程中确定哪些属性应该被递归地验证
public interface TraversableResolver {
    // 是否应该遍历一个对象的属性
    boolean isReachable(Object var1, Path.Node var2, Class<?> var3, Path var4, ElementType var5);
    // 是否应该对属性值进行递归验证
    boolean isCascadable(Object var1, Path.Node var2, Class<?> var3, Path var4, ElementType var5);
}
  • SpringBoot 默认情况下确实会使用 JPATraversableResolver 作为 TraversableResolver 的实现

  • image.png

  • JPATraversableResolver 默认会对所有对象进行递归验证

public class JPATraversableResolver implements TraversableResolver {
    private static final Log LOG = LoggerFactory.make(MethodHandles.lookup());

    public JPATraversableResolver() {
    }

    public final boolean isReachable(Object traversableObject, Path.Node traversableProperty, Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType) {
        if (LOG.isTraceEnabled()) {
            LOG.tracef("Calling isReachable on object %s with node name %s.", traversableObject, traversableProperty.getName());
        }

        return traversableObject == null ? true : Persistence.getPersistenceUtil().isLoaded(traversableObject, traversableProperty.getName());
    }

    public final boolean isCascadable(Object traversableObject, Path.Node traversableProperty, Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType) {
        // 直接返回 true
        return true;
    }
}

扩展

  • 上述我们看到的参数校验方式称之为: 方法级别的参数校验,即在 "Service" 层使用 @Validate 和 @Valid 注解实现校验。背后原理就是: MethodValidationPostProcessor 通过AOP技术拦截方法调用,并使用 MethodValidationInterceptor 对切点方法织入增强,执行校验逻辑

除此之外,还有对 RequestBody 的参数校验

  • 针对 RequestBody 的参数校验,SpringMVC 有一套自己的实现机制。在 RequestResponseBodyMethodProcessor 在解析参数时会调用 validateIfApplicable 方法进行校验
    protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
        // ...

        for(int var6 = 0; var6 < var5; ++var6) {
            Annotation ann = var4[var6];
            Validated validatedAnn = (Validated)AnnotationUtils.getAnnotation(ann, Validated.class);
            // 可以发现这个校验支持: Validated 注解,以及以 Valid 开头的注解(因此可以通过自定义校验注解实现自定义校验,后续会将)
            if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
                // ...
                // 校验
                binder.validate(validationHints);
                break;
            }
        }
    }

参考文章