博客 / 詳情

返回

死磕Spring Boot Validation校驗

一、基本介紹

SpringBoot提供了方便的validation主要對輸入數據進行校驗,確保數據符合預期規則,是保證應用健壯性的重要手段,
1、Bean Validation:基於 JSR-380 (Bean Validation 2.0) 規範、
2、Hibernate Validator:最流行的實現
3、Spring 集成:通過 @Valid 或 @Validated 註解觸發驗證
怎麼使用就不介紹了,包含如何自定義註解進行校驗,分組驗證,處理驗證錯誤

二、javax.validation

這裏項目jdk為1.8,所使用的包名為javax.validation,之後的版本變更為jakarta.validation
這個包為Jakarta EE平台的基礎核心包之一,提供驗證bean標準的API,
總入口為Validation類,作為標準的api,需要暴露接口供其他包進行接入,接口為ValidationProvider
image
ValidationProvider通過ValidationProviderResolver進行處理,除此之外,javax.validation提供了默認的處理器DefaultValidationProviderResolver
會通過SPI機制ServiceLoader加載META-INF/services/
如果未加載到則會拋出異常,否則會取第一個ValidationProvider
image
最終通過configure生成javax.validation.Configuration
Configuration也提供了非常多的接口層定義,需要實現buildValidatorFactory,再通過ValidatorFactory.getValidator進行校驗
javax.validation提供了一些基礎的校驗註解,具體校驗規則也需要單獨實現
image

三、hibernate實現

首先在META-INF/services目錄下申明javax.validation.spi.ValidationProvider為org.hibernate.validator.HibernateValidator
image
HibernateValidator生成的configuration為HibernateValidatorConfiguration
image
ValidatorFactory的實現為ValidatorFactoryImpl
其中含有幾個重要的屬性

1、ConstraintValidatorFactory

負責ConstraintValidator的創建和生命週期,通過工廠獲取某個校驗的ConstraintValidator實例,如果是spring項目,使用的是SpringConstraintValidatorFactory有springframework負責實現

2、校驗邏輯

直到開始校驗時才會執行Validator.validate方法
image
這裏以分組校驗對象為例,Validator也提供了很多種靈活的校驗,包括校驗單獨的某個屬性
其中BeanMetaData主要通過AnnotationMetaDataProvoder進行註解的元數據獲取,主要思路為根據constraintHelper.isConstraintAnnotation是否當前類含有校驗屬性的註解Constraint.class,因為基本上每個校驗註解裏面都有@Constraint
如果沒有任何約束條件,則會直接結束,同時,BeanMetaData進行了緩存,下一次校驗同類型的時候直接從緩存獲取metaData
緊接着會對校驗的組進行排序,每次校驗可以支持單個或者多個,如果未指定,默認是javax.validation.groups.Default
最後會執行validateInContext進行校驗,其中短路驗證shouldFailFast,是hibernate專有的,如果開啓了這個屬性,遇到驗證失敗的則會直接結束,不再往下執行
這裏就會用到提供的接口所有實現ConstraintValidator,調用isValid方法
ConstraintTree#validateSingleConstraint
image
如果校驗失敗,開始構建約束違反的消息,主要處理類在AnnotationDescriptor#getMandatoryAttribute,主要獲取註解的message屬性
如果message開頭不是{,而是自己定義的比如姓名不能為空,則會直接返回message,否則,會通過AbstractMessageInterpolator#interpolateMessage將消息通過本地語言設置進行解析
image
在resource下面進行提取
image
最好將校驗失敗的結果放入Set
至此,這個校驗過程就算結束了
可以單獨申明Hiberator的校驗器,提取為公用的工具使用

/**
     * Hibernate校驗器
     */
    private static Validator hibernateValidator = Validation.byProvider(HibernateValidator.class).
            configure().failFast(true).buildValidatorFactory().getValidator();
/**
     * Hibernate校驗器
     *
     * @param obj         被校驗實體或字段
     * @param detailError 是否輸出字段等詳細信息
     * @param groups      校驗組
     */
    public static void validate(Object obj, boolean detailError, Class<?>... groups) throws ValidationException {
        Set<ConstraintViolation<Object>> constraintViolations = hibernateValidator.validate(obj, groups);
        //判斷校驗返回的錯誤信息集合是否為空
        if (CollectionUtils.isEmpty(constraintViolations)) {
            return;
        }
        ConstraintViolation<Object> constraintViolation = constraintViolations.iterator().next();
        String errorMsg = constraintViolation.getMessage();
        Object invalidateValue = constraintViolation.getInvalidValue();
        Object propertyPath = constraintViolation.getPropertyPath();
        log.error("字段【{}】校驗失敗,其值為:\"{}\",不符合規則【{}】", propertyPath, invalidateValue, errorMsg);
        if (detailError) {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("字段");
            if (propertyPath != null) {
                stringBuilder.append("【");
                stringBuilder.append(propertyPath);
                stringBuilder.append("】");
            }
            stringBuilder.append("校驗失敗,其值為:\"");
            stringBuilder.append(invalidateValue);
            stringBuilder.append("\",不符合規則【");
            stringBuilder.append(errorMsg);
            stringBuilder.append("】");
            errorMsg = stringBuilder.toString();
        }
        throw new BaseRuntimeException(99999, errorMsg);
    }

四、SpringBoot項目校驗

在spring-boot-autoconfigure包下面提供了非常多的自動配置,validation也是其中的一環,主配置類為ValidationAutoConfiguration
image
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
這兩個註解就不必多介紹了,第一個需要可執行的校驗實例,而這個如果引入了hiberate,則是ValidatorImpl實現了ExecutableValidator
第二個也一樣,如果引入了hiberate,javax.validation.spi.ValidationProvider為org.hibernate.validator.HibernateValidator
其中PrimaryDefaultValidatorPostProcessor會在@Confuguration將ValidationAutoConfiguration裏面的@Bean方法注入到BeanDefinition之後,判斷是否已經存在prifary的org.springframework.validation.Validator bean類型
如果不存在,則會將defaultValidator設置為primary
springboot默認自動配置的javax.validation.Validator為LocalValidatorFactoryBean,值得注意的是這個應該使用的類似裝飾器模式,在裏面申明瞭ValidatorFactory,通過ValidatorFactory來得到具體執行的Validator,如果引用了hibernate,則執行器為ValidatorImpl,因為將Validator注入到bean了,如果需要顯示的調用,我們可以執行引用bean

 @Autowired
 private Validator validator;

到這裏我們只是將Validator注入到spring容器當中了,只能顯示的調用validator.validate方法,但是spring有aop可不建議這麼做
第二個自動裝配的是MethodValidationPostProcessor,之前我們常用的就是在controller接口層添加@Validated註解,再添加各種限制條件註解,MethodValidationPostProcessor負責處理這個邏輯
由於繼承了AbstractAdvisingBeanPostProcessor,可以自動為匹配的bean創建aop代理,將配置的advice應用到目標bean上
首先在InitializingBeand的afterPropertiesSet會優先定義pointcut和advisor,切入點是含有@Validated註解的類或者方法

Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);

其中切面的攔截器是MethodValidationInterceptor,傳入validator,一般常用的controller裏面當有請求過來時,會優先走下面的攔截器進行參數的驗證方法
image
接着由於實現了BeanPostProcessor,執行完開始執行afterPropertiesSet方法開始執行postProcessAfterInitialization,這個方法會將申明的advised添加bean的切面裏面

像MethodValidationPostProcessor這種已經相當於模板方法了,像@Async異步任務和上面的是同理,@Scheduled定時調度也差不多這個道理實現的切面

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.