拓展 LocalValidatorFactoryBean指定自定义值提取器?看看 Spring Boot 开发团队怎么说
本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
一般的,Spring Boot 项目引入 JavaEE 规范下 validation-api 的具体实现依赖比如 hibernate-validator,就能实现方法层面基于注解的参数、结果校验,比如:
// 实现类要加 @Validated 注解
public interface PersonService {
@NotNull Person getOne(@NotBlank String name);
List<@Valid @NotNull Person> getList();
Result<@Valid @NotNull Person> getResult();
}
public class Person {
@NotNull
private String name;
}
- 对于方法
getOne,如果传参name为空或返回结果为null,则会抛出校验异常 - 对于方法
getList,如果返回结果集合中有NULL或结果集不符合校验规则(比如Person.name为空),则会抛出校验异常 - 而对于
getResult方法,我们想要它和getList的校验规则相同在默认情况下是不可行的(因为Validator不知道如何从自定义结果集中获取结果) - 如果我们想让
getResult方法实现我们预期的校验效果,则需要实现自定义的ValueExtractor并基于此获取对应的Validator,而本文也是打算基于Spring提供的LocalValidatorFactoryBean进行拓展,来实现这一目的
ValidationAutoConfiguration
@Configuration(proxyBeanMethods = false)
// 依赖于类路径的 ExecutableValidator
@ConditionalOnClass(ExecutableValidator.class)
// 依赖于实现类的对应文件(比如 hibernate-validator)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {
// Spring 对 ValidatorFactory Validator 等组件的注册都封装在 LocalValidatorFactoryBean 中
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(Validator.class)
public static LocalValidatorFactoryBean defaultValidator(ApplicationContext applicationContext) {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(applicationContext);
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
// 这里就是 Spring 下可以基于注解开启方法参数校验的关键
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
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;
}
}
这是 SpringBoot 提供的 validation 自动装配类
- 通常,在我们引入
JavaEE规范下validation-api的具体实现依赖比如hibernate-validator后,该类会被自动装配(不能引入太新的版本,可能因为javax相关包路径修改为Jakarta导致该类无法装配) - 组件
LocalValidatorFactoryBean里封装了Spring对validation-api的ValidatorFactoryValidator等组件的注册,也是我们本文要拓展的对象 MethodValidationPostProcessor是Spring基于@Validated注解就能实现方法参数、结果校验的关键处理类
AddValueExtractorLocalValidatorFactoryBean
public class AddValueExtractorLocalValidatorFactoryBean extends LocalValidatorFactoryBean {
List<ValueExtractor> valueExtractors;
public List<ValueExtractor> getValueExtractors() {
return valueExtractors;
}
public void setValueExtractors(List<ValueExtractor> valueExtractors) {
this.valueExtractors = valueExtractors;
}
@Override
protected void postProcessConfiguration(Configuration<?> configuration) {
// 添加所有自定义的 ValueExtractor
Optional.ofNullable(this.getValueExtractors())
.ifPresent((list) -> list.forEach(configuration::addValueExtractor));
}
}
我们继承 LocalValidatorFactoryBean 类对 postProcessConfiguration 方法进行拓展:允许传入一组 ValueExtractor 并将其添加到 Validation Configuration 中
ResultValueExtractor
@Component
public class ResultValueExtractor implements ValueExtractor<Result<@ExtractedValue ?>> {
@Override
public void extractValues(Result<?> result, ValueReceiver valueReceiver) {
valueReceiver.value(null, result.getData());
}
}
添加一个自定义的 ValueExtractor 并将其注册为 bean 组件方便容器收集
BeanValidationConfig
@Configuration
public class BeanValidationConfig {
@Bean
public static LocalValidatorFactoryBean defaultValidator(
ApplicationContext applicationContext
, List<ValueExtractor> valueExtractors
) {
// 这里注册的是我们自己拓展的 AddValueExtractorLocalValidatorFactoryBean
AddValueExtractorLocalValidatorFactoryBean factoryBean = new AddValueExtractorLocalValidatorFactoryBean();
factoryBean.setValueExtractors(valueExtractors);
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(applicationContext);
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
@Bean
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;
}
}
- 这里实际就是复制了一份
ValidationAutoConfiguration中的配置 - 当然,其中的
LocalValidatorFactoryBean注册的是被我们拓展过的AddValueExtractorLocalValidatorFactoryBean - 这里会收集容器中所有自定义的
ValueExtractor,将其添加进去 - 至此,我们可以对文首示例中
getResult方法返回的自定义结果集进行校验了
值得一提的是,这里相当于我们完全覆盖了 SpringBoot 提供的自动装配类,实际上还是有点不方便的,要是 SpringBoot(SpringFramework)提供现成的子类并自动装配,那是极好的
出于此目的我也提了对应的 PR 给 SpringBoot,并基于开发团队的建议提供了 Customizer 回调机制进行优化
MethodValidationPostProcessor
写都写了,就顺便聊一下 MethodValidationPostProcessor
- 如上是它的类继承图,
MethodValidationPostProcessor是ProxyProcessorSupport下AbstractAdvisingBeanPostProcessor分支的一个实现,该分支是基于Advisor进行代理增强,子类提供具体的Advisor,比如AsyncAnnotationBeanPostProcessor和MethodValidationPostProcessor - 同时提一嘴
ProxyProcessorSupport的另一个分支AbstractAutoProxyCreator,它是对符合条件的bean组件进行代理,子类聚焦于实现获取符合条件的bean,比如Spring AOP @Aspect的实现
private Class<? extends Annotation> validatedAnnotationType = Validated.class;
@Override
public void afterPropertiesSet() {
// Pointcut 是基于 @Validated 注解的 AnnotationMatchingPointcut
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
// Advice 是 MethodValidationInterceptor
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
- 通俗的理解,
Advisor = Pointcut + Advice,前者决定切入点(即具体方法),后者对应通知逻辑(此处对应方法的参数、返回值校验) MethodValidationPostProcessor的Pointcut是基于@Validated注解的AnnotationMatchingPointcut,这就是之所以校验类上要加@Validated注解(该注解由Spring提供)MethodValidationPostProcessor的Advice是MethodValidationInterceptor,它负责对应方法的参数、返回值校验
MethodValidationInterceptor
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// FactoryBean 相关方法跳过
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
Class<?>[] groups = determineValidationGroups(invocation);
// 相关校验依赖于 ExecutableValidator API
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<ConstraintViolation<Object>> result;
Object target = invocation.getThis();
Assert.state(target != null, "Target must not be null");
try {
// 参数校验
result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
// ...
}
// 校验失败抛异常
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 进行参数、返回值处理,校验失败抛出 ConstraintViolationException 异常
总结
本文主要:
- 基于
Spring提供的LocalValidatorFactoryBean进行拓展,以支持自定义结果集的校验 - 简单解析了
MethodValidationPostProcessor以分析Spring基于注解的方法参数、返回值校验