最近要做動態資料的提交處理,即需要分析提交資料欄位定義資訊後才能明確對應的具體欄位型別,進而做資料型別轉換和欄位有效性校驗,然後做業務處理後提交資料庫,自己開發一套校驗邏輯的話週期太長,因此分析了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的請求呼叫流程 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方法校驗