Spring Validation實現原理分析

68號小喇叭發表於2018-07-08

最近要做動態資料的提交處理,即需要分析提交資料欄位定義資訊後才能明確對應的具體欄位型別,進而做資料型別轉換和欄位有效性校驗,然後做業務處理後提交資料庫,自己開發一套校驗邏輯的話週期太長,因此分析了Spring Validation的實現原理,複用了其底層花樣繁多的Validator,在此將分析Spring Validation原理的過程記錄下,不深入細節

如何使用Spring Validation
  • Spring Bean初始化時校驗Bean是否符合JSR-303規範
    1、手動新增BeanValidationPostProcessor Bean
    2、在model類中定義校驗規則,如@Max、@Min、@NotEmpty
    3、宣告Bean,綜合程式碼如下:
@Bean
public BeanPostProcessor beanValidationPostProcessor() {
    return new BeanValidationPostProcessor();
}

@Bean
public UserModel getUserModel() {
    UserModel userModel = new UserModel();
    userModel.setUsername(null);
    userModel.setPassword("123");
    return userModel;
}

@Data
class UserModel {
    @NotNull(message = "username can not be null")
    @Pattern(regexp = "[a-zA-Z0-9_]{5,10}", message = "username is illegal")
    private String username;
    @Size(min = 5, max = 10, message = "password's length is illegal")
    private String password;
}
複製程式碼

4、BeanValidationPostProcessor Bean內部有個boolean型別的屬性afterInitialization,預設是false,如果是false,在postProcessBeforeInitialization過程中對bean進行驗證,否則在postProcessAfterInitialization過程對bean進行驗證
5、此種校驗使用了spring的BeanPostProcessor邏輯,可參考Spring Boot系列之一:如何快速熟悉Spring技術棧
6、校驗底層呼叫了doValidate方法,進一步呼叫validator.validate,預設validator為HibernateValidator,validation-api包為JAVA規範,Spring預設的規範實現為hibernate-validator包,此hibernate非ORM框架Hibernate

protected void doValidate(Object bean) {
	Assert.state(this.validator != null, "No Validator set");
	Set<ConstraintViolation<Object>> result = this.validator.validate(bean);
複製程式碼

7、HibernateValidator預設呼叫ValidatorFactoryImpl來生成validator,後面展開將ValidatorFactoryImpl

  • 支援方法級別的JSR-303規範
    1、手動新增MethodValidationPostProcessor Bean
    2、類上加上@Validated註解(也支援自定義註解,建立MethodValidationPostProcessor Bean時傳入)
    3、在方法的引數中加上驗證註解,比如@Max、@Min、@NotEmpty、@NotNull等,如
@Component
@Validated
public class BeanForMethodValidation {
    public void validate(@NotEmpty String name, @Min(10) int age) {
        System.out.println("validate, name: " + name + ", age: " + age);
    }
}  
複製程式碼

4、MethodValidationPostProcessor內部使用aop完成對方法的呼叫

public void afterPropertiesSet() {
    Pointcut pointcut = new `AnnotationMatchingPointcut`(this.validatedAnnotationType, true);
    this.advisor = new `DefaultPointcutAdvisor`(pointcut, createMethodValidationAdvice(this.validator));
}
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
	return (validator != null ? new `MethodValidationInterceptor`(validator) : new MethodValidationInterceptor());
}
複製程式碼

5、底層同樣預設呼叫ValidatorFactoryImpl來生成validator,由validator完成校驗

  • 直接編碼呼叫校驗邏輯,如
public class Person {
@NotNull(message = "性別不能為空")
private Gender gender;
@Min(10)
private Integer age;
...
}
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
Person person = new Person();
person.setGender(Gender.Man);
validator.validate(person);
複製程式碼

同上,預設呼叫ValidatorFactoryImpl來生成validator,由validator完成具體校驗

  • 在Spring controller方法引數中使用valid或validated註解標註待校驗引數
    1、先熟悉下Spring的請求呼叫流程
    Spring Validation實現原理分析
    2、可以看到在各種resolver處理請求引數的過程中做了引數校驗
    3、底層統一呼叫了DataBinder的validate方法
    4、DataBinder的作用:Binder that allows for setting property values onto a target object, including support for validation and binding result analysis,也就是binder處理了request提交的字串形式的引數,將其轉換成服務端真正需要的型別,binder提供了對validation的支援,可以存放校驗結果
    5、DataBinder的validator預設在ConfigurableWebBindingInitializer中初始化,預設使用OptionalValidatorFactoryBean,該Bean繼承了LocalValidatorFactoryBean,LocalValidatorFactoryBean組合了ValidatorFactory、自定義校驗屬性等各種校驗會用到的資訊,預設使用ValidatorFactoryImpl來獲取validator
至此,所有的線索都指向了ValidatorFactoryImpl,下面分析下該類
public Validator `getValidator`() {
	return `createValidator`(
		constraintValidatorManager.getDefaultConstraintValidatorFactory(),
		valueExtractorManager,
		validatorFactoryScopedContext,
		methodValidationConfiguration
	);
}
Validator `createValidator`(ConstraintValidatorFactory constraintValidatorFactory,
	ValueExtractorManager valueExtractorManager,
	ValidatorFactoryScopedContext validatorFactoryScopedContext,
	MethodValidationConfiguration methodValidationConfiguration) {
	
	BeanMetaDataManager beanMetaDataManager = beanMetaDataManagers.computeIfAbsent(
		new BeanMetaDataManagerKey( validatorFactoryScopedContext.getParameterNameProvider(), valueExtractorManager, methodValidationConfiguration ),
		key -> new BeanMetaDataManager(
			`constraintHelper`,
			executableHelper,
			typeResolutionHelper,
			validatorFactoryScopedContext.getParameterNameProvider(),
			valueExtractorManager,
			validationOrderGenerator,
			buildDataProviders(),
			methodValidationConfiguration
		)
	 );
   
        return `new ValidatorImpl`(
			constraintValidatorFactory,
			beanMetaDataManager,
			valueExtractorManager,
			constraintValidatorManager,
			validationOrderGenerator,
			validatorFactoryScopedContext
	);
}
public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
	Contracts.assertNotNull( object, MESSAGES.validatedObjectMustNotBeNull() );
	sanityCheckGroups( groups );

	ValidationContext<T> validationContext = `getValidationContextBuilder().forValidate( object )`;

	if ( !validationContext.getRootBeanMetaData().hasConstraints() ) {
		return Collections.emptySet();
	}

	ValidationOrder validationOrder = determineGroupValidationOrder( groups );
	ValueContext<?, Object> valueContext = `ValueContext.getLocalExecutionContext`(
			validatorScopedContext.getParameterNameProvider(),
			object,
			validationContext.getRootBeanMetaData(),
			PathImpl.createRootPath()
	);

	return validateInContext( validationContext, valueContext, validationOrder );
}
複製程式碼

1、getValidator->createValidator->ValidatorImpl->validate
在執行過程中封裝了beanMetaDataManager、validationContext、valueContext等內容,都是校驗時會用到的上下文資訊,如待校驗bean的所有校驗項(含父類和介面)、property、method parameter的校驗資訊,從ValidatorFactoryScopedContext繼承過來的validator通用的各種工具類(如message、script等的處理)等,內容比較複雜
2、分組(group)校驗忽略,來到預設分組處理validateConstraintsForDefaultGroup->validateConstraintsForSingleDefaultGroupElement->validateMetaConstraint(注:metaConstraints維護了該bean型別及其父類、介面的所有校驗,需要遍歷呼叫validateMetaConstraint)
3、繼續呼叫MetaConstraint的doValidateConstraint方法,根據不同的annotation type走不同的ConstraintTree

public static <U extends Annotation> ConstraintTree<U> of(ConstraintDescriptorImpl<U> composingDescriptor, Type validatedValueType) {
	if ( composingDescriptor.getComposingConstraintImpls().isEmpty() ) {
		return new SimpleConstraintTree<>( composingDescriptor, validatedValueType );
	}
	else {
		return new ComposingConstraintTree<>( composingDescriptor, validatedValueType );
	}
}
複製程式碼

4、具體哪些走simple,哪些走composing暫且不管,因為二者都呼叫了ConstraintTree的'getInitializedConstraintValidator'方法,該步用來獲取校驗annotation(如DecimalMax、NotEmpty等)對應的validator並初始化validator 5、ConstraintHelper類維護了所有builtin的validator,並根據校驗annotation(如DecimalMax)分類,validator的描述類中維護了該validator的泛型模板(如BigDecimal),如下:

putConstraints( tmpConstraints, DecimalMax.class,  Arrays.asList(
	DecimalMaxValidatorForBigDecimal.class,
	DecimalMaxValidatorForBigInteger.class,
	DecimalMaxValidatorForDouble.class,
	DecimalMaxValidatorForFloat.class,
	DecimalMaxValidatorForLong.class,
	DecimalMaxValidatorForNumber.class,
	DecimalMaxValidatorForCharSequence.class,
	DecimalMaxValidatorForMonetaryAmount.class
) );
複製程式碼

在獲取具體bean類的validator時,先根據annotation獲取所有的validator,對應方法是ConstraintManager.findMatchingValidatorDescriptor,然後根據被校驗物件的型別獲取唯一的validator
6、然後根據上下文資訊initializeValidator,進而呼叫validator的isValid方法校驗

歡迎關注我的微信公眾號

68號小喇叭

相關文章