[Spring] expand LocalValidatorFactoryBean under Spring Boot to realize the parsing of custom results

Posted by Kilo on Sun, 16 Jan 2022 22:53:02 +0100

preface

Generally, the Spring Boot project introduces the specific implementation dependency of the validation API under the Java EE specification, such as hibernate validator, to implement annotation based parameter and result verification at the method level, such as:

// The implementation class should be annotated with @ 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;

}
  • For the method getOne, if the passed parameter name is empty or the returned result is null, a verification exception will be thrown
  • For the method getList, if there is NULL in the returned result set or the result set does not comply with the verification rules (for example, Person.name is empty), a verification exception will be thrown
  • For the getResult method, we want it to have the same verification rules as getList, which is not feasible by default (because the Validator does not know how to get the results from the custom result set)
  • If we want the getResult method to achieve our expected verification effect, we need to implement a custom ValueExtractor and obtain the corresponding Validator based on it. This article also intends to expand based on the LocalValidatorFactoryBean provided by Spring to achieve this purpose

ValidationAutoConfiguration

@Configuration(proxyBeanMethods = false)
// ExecutableValidator dependent on classpath
@ConditionalOnClass(ExecutableValidator.class)
// The corresponding file that depends on the implementation class (such as hibernate validator)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {

	// Spring's registration of ValidatorFactory Validator and other components is encapsulated in 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;
	}

	// Here is the key to enable method parameter verification based on annotations in 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;
	}

}

This is the validation auto assembly class provided by SpringBoot

  • Generally, after we introduce the specific implementation dependencies of the validation API under the Java EE specification, such as hibernate validator, this class will be automatically assembled (too new version can not be introduced, possibly because the javax related package path is modified to Jakarta, so this class cannot be assembled)
  • The component LocalValidatorFactoryBean encapsulates Spring's registration of ValidatorFactory Validator and other components of the validation API, which is also the object to be expanded in this article
  • MethodValidationPostProcessor is a key processing class that Spring can implement method parameter and result verification based on @ Validated annotation

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) {
        // Add all custom valueextractors
        valueExtractors.forEach(configuration::addValueExtractor);
    }
}

We extend the postProcessConfiguration method by inheriting the LocalValidatorFactoryBean class: allowing a set of valueextractors to be passed in and added to the Validation Configuration

ResultValueExtractor

@Component
public class ResultValueExtractor implements ValueExtractor<Result<@ExtractedValue ?>> {

    @Override
    public void extractValues(Result<?> result, ValueReceiver valueReceiver) {
        valueReceiver.value(null, result.getData());
    }
}

Add a custom ValueExtractor and register it as a bean component to facilitate container collection

BeanValidationConfig

@Configuration
public class BeanValidationConfig {

    @Bean
    public static LocalValidatorFactoryBean defaultValidator(
            ApplicationContext applicationContext
            , List<ValueExtractor> valueExtractors
    ) {
        // Registered here is our own extended 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;
    }
}
  • This is actually a copy of the configuration in ValidationAutoConfiguration
  • Of course, the localvalidator factorybean is registered with the addvalueextractorlocalvalidator factorybean that we have expanded
  • All custom valueextractors in the container will be collected and added here
  • At this point, we can verify the custom result set returned by the getResult method in the initial example

MethodValidationPostProcessor


After all, let's talk about the method validation postprocessor

  • The above is its class inheritance diagram. MethodValidationPostProcessor is an implementation of the abstractadvising beanpostprocessor branch under ProxyProcessorSupport. This branch is proxy enhanced based on Advisor. Subclasses provide specific advisors, such as AsyncAnnotationBeanPostProcessor and MethodValidationPostProcessor
  • At the same time, mention AbstractAutoProxyCreator, another branch of ProxyProcessorSupport. It proxies qualified bean components. Subclasses focus on obtaining qualified beans, such as the implementation of Spring AOP @Aspect
	private Class<? extends Annotation> validatedAnnotationType = Validated.class;

	@Override
	public void afterPropertiesSet() {
		// Pointcut is an annotation matching pointcut based on the @ Validated annotation 
		Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
		this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
	}

	// Advice is the MethodValidationInterceptor
	protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
		return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
	}
  • In popular understanding, Advisor = Pointcut + Advice. The former determines the entry point (i.e. specific method), and the latter corresponds to the notification logic (here corresponds to the parameter and return value verification of the method)
  • The Pointcut of MethodValidationPostProcessor is an annotationmatching Pointcut based on the @ Validated annotation, which is why the @ Validated annotation is added to the verification class (provided by Spring)
  • The Advice of the MethodValidationPostProcessor is the MethodValidationInterceptor, which is responsible for verifying the parameters and return values of the corresponding method

MethodValidationInterceptor

	@Override
	@Nullable
	public Object invoke(MethodInvocation invocation) throws Throwable {
		// FactoryBean related methods skip
		if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
			return invocation.proceed();
		}

		Class<?>[] groups = determineValidationGroups(invocation);

		// The related validation depends on the 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 {
			// Parameter verification
			result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
		}
		catch (IllegalArgumentException ex) {
			// ...
		}
		// Verification failure throw exception
		if (!result.isEmpty()) {
			throw new ConstraintViolationException(result);
		}

		Object returnValue = invocation.proceed();

		// Return value verification
		result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups);
		// Verification failure throw exception
		if (!result.isEmpty()) {
			throw new ConstraintViolationException(result);
		}

		return returnValue;
	}

The specific notification logic is relatively simple. It depends on the ExecutableValidator to process the parameters and return values. If the verification fails, a ConstraintViolationException is thrown

summary

This paper mainly:

  • It is extended based on the LocalValidatorFactoryBean provided by Spring to support the verification of custom result sets
  • The MethodValidationPostProcessor is simply parsed to analyze Spring annotation based method parameters and return value verification

For the understanding of Advisor Pointcut Advice, etc., you can refer to the link

[source code] Spring AOP 2 Advice

[source code] Spring AOP 4 Pointcut

[source code] Spring AOP 5 Advisor

Topics: Java Spring Spring Boot