@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
通过类名显然可以发现这是一个后置处理器,在 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 的实现
-
-
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;
}
}
}