拓展 LocalValidatorFactoryBean指定自定义值提取器?看看 Spring Boot 开发团队怎么说

444 阅读5分钟

拓展 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 里封装了 Springvalidation-apiValidatorFactory Validator 等组件的注册,也是我们本文要拓展的对象
  • MethodValidationPostProcessorSpring 基于@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

  • 如上是它的类继承图,MethodValidationPostProcessorProxyProcessorSupportAbstractAdvisingBeanPostProcessor 分支的一个实现,该分支是基于 Advisor 进行代理增强,子类提供具体的 Advisor,比如 AsyncAnnotationBeanPostProcessorMethodValidationPostProcessor
  • 同时提一嘴 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,前者决定切入点(即具体方法),后者对应通知逻辑(此处对应方法的参数、返回值校验)
  • MethodValidationPostProcessorPointcut 是基于 @Validated 注解的 AnnotationMatchingPointcut,这就是之所以校验类上要加 @Validated 注解(该注解由 Spring 提供)
  • MethodValidationPostProcessorAdviceMethodValidationInterceptor,它负责对应方法的参数、返回值校验

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 基于注解的方法参数、返回值校验