Hibernate Validator -對象基礎驗證(一)(可能是東半球最全的講解了)

需求背景

最近在做和Excel導入、導出相關的需求,需求是對用戶上傳的整個Excel的數(shù)據進行驗證,如果不符合格式要求,在表頭最后增加一列“錯誤信息”描述此行數(shù)據錯誤原因。例如:代理人列不能為空;代理人手機號列如果填寫有值肯定是需要符合手機號格式;結算方式列只允許填寫“全保費、凈保費”字段...

初始想法

其實這些需求在之前是已經實現(xiàn)過了的,只不過周期比較短,小組內每個童鞋都各自定義了一套自己的規(guī)則去實現(xiàn)這些邏輯;我去看了下也都大同小異,無非是將Excel數(shù)據全部讀進來,轉化為List<Map<String, Object>>,多線程去校驗每行數(shù)據的準確性,將校驗結果寫入CopyOnWriteArrayList(線程安全),大體實現(xiàn)邏輯如下:

//ImportMessageDetail為自定義的錯誤類,方便追加Excel錯誤列使用
List<ImportMessageDetail> resultList =new CopyOnWriteArrayList<>();
   mapList.parallelStream().forEach(map ->{
     ImportMessageDetail detail = validate(map, checkModel.getColumnReference(),
           checkModel.getCheckSet(), rowNoMap, dataValidateType);
     if (detail !=null) {
         resultList.add(detail);
     }
});

Hibernate Validator

簡單介紹

RESTful的接口服務中,會有各種各樣的入參,我們不可能完全不做任何校驗就直接進入到業(yè)務處理的環(huán)節(jié),通常我們會有一個基礎的數(shù)據驗證的機制,待這些驗證過程完畢,結果無誤后,參數(shù)才會進入到正式的業(yè)務處理中。而數(shù)據驗證又分為兩種,一種是無業(yè)務關聯(lián)的規(guī)則性驗證,一種是根據現(xiàn)有數(shù)據進行的聯(lián)動性數(shù)據驗證(簡單來說,參數(shù)的合理性以及需要查數(shù)據庫驗證的邏輯業(yè)務)。而Hibernate-Validator則適合做無業(yè)務關聯(lián)的規(guī)則性驗證,而這類驗證的代碼大多是可復用的。

簡單來說,就是Java規(guī)定了一套關于驗證器的接口。

項目引入Hibernate Validator

  • maven
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.2</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
    <version>2.1.12</version>
</dependency>
  • gradle
compile group: 'javax.el', name: 'javax.el-api', version: '3.0.0'
compile group: 'org.glassfish.web', name: 'javax.el', version: '2.2.6'
compile group: 'org.hibernate', name: 'hibernate-validator', version: '5.4.1.Final'
  • 如果本身是Spring Boot項目,無需單獨引入,Spring Boot項目中包含此jar包

Spring boot下配置ValidatorFactory

@Configuration
public class ValidatorFactory {
    @Bean
    @ConditionalOnBean(Validator.class)
    public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) {
        javax.validation.ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory))
                //.addProperty("hibernate.validator.fail_fast", "true") // 只要有一個驗證失敗,則返回
                .buildValidatorFactory();

        return validatorFactory.getValidator();
    }
}

注:Hibernate Validator有兩種驗證模式
1.普通模式,會校驗完所有的屬性,然后返回所有的校驗結果,默認就是這種方式
2.快速失敗返回模式,遇到校驗不通過的,直接返回,將上述代碼中.addProperty("hibernate.validator.fail_fast", "true")放開即可

簡單測試

  • 需要驗證的bean
@Data
public class Student {

    private Long id;

    @NotBlank(message = "姓名不能為空")
    private String name;

    @NotNull
    @Min(value = 5, message = "年齡不能低于5歲")
    private int age;

    private String stuNo;

    @NotNull
    @Digits(integer = 10, fraction = 2, message = "請保留小數(shù)點后2位")
    private BigDecimal salary;

}
  • 基于Spring boot的測試類
@SpringBootTest(classes = SpringApplicationLauncher)
class StudentValidator extends Specification {

    @Autowired
    private Validator validator

    def testStudent() {
        Student student = new Student();
        student.setName("");
        student.setAge(3)
        student.setSalary(new BigDecimal("26.98765").setScale(4, RoundingMode.HALF_UP))

        //整個對象完全校驗
        Set<ConstraintViolation<Student>> result = validator.validate(student);
        printfError(result);
        //只校驗某個屬性
        Set<ConstraintViolation<Student>> result2 = validator.validateProperty(student, "age");
        printfError(result2);
        //主動校驗某個屬性值是否合規(guī)
        Set<ConstraintViolation<Student>> result3 = validator.validateValue(Student.class, "salary", new BigDecimal("123.3434"));
        printfError(result3);

        expect:
        true
    }

    def printfError(Set<ConstraintViolation<Student>> result) {
        System.out.println("================");
        for (ConstraintViolation it : result) {
            System.out.println(it.message);
        }
    }

}
  • 控制臺打印結果
年齡不能低于5歲
姓名不能為空
請保留小數(shù)點后2位
================
年齡不能低于5歲
================
請保留小數(shù)點后2位

內置的校驗注解

內置校驗注解的位置.jpg

具體注解如何使用請查看源碼注釋,養(yǎng)成讀源碼的習慣
有幾個地方需要跟大家闡述一下

  • 除了@Empty要求字符串不能全是空格,其他的字符串校驗都是允許空格的
  • message是可以引用常量的,但是如@Size里max不允許引用對象常量,基本類型常量是可以的
  • 注意大部分規(guī)則校驗都是允許參數(shù)為null,即當不存在這個值時,就不進行校驗了
    另外有幾個注解概念容易混淆:
  • @NotEmpty : 加了@NotEmpty的String類、Collection、Map、數(shù)組,是不能為null或者長度為0的(String Collection Map的isEmpty()方法)
  • @NotBlank:只用于String,不能為null且trim()之后size>0
  • @NotNull: 不能為null,但可以為empty,沒有Size的約束
某些注解滿足不了現(xiàn)有需求,需要自定義注解

自定義注解

  • 創(chuàng)建約束性注解
  • 約束性注解實現(xiàn)類
  • 注解使用在指定的屬性或者類上
    需求:在保單類型字段上只允許填寫具體的文本,填寫其他內容均提示錯誤
/**
 * author:Java
 * Date:2020/5/28 15:24
 * 校驗某個字段只能是固定的某些值
 * 比如:是否有車牌,只能填寫“是”或者“否”
 * 如果是某個枚舉類型里面的text值,就設置enumClass;如果是自定義的某些值,就設置values數(shù)組
 */
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdentifyFieldValueValidator.class)
@Documented
public @interface IdentifyFieldValue {

    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default { };

    /**
     * 某個枚舉類型
     * @return
     */
    Class enumClass() default DefaultEnum.class;

    /**
     * 固定的某些值
     */
    String[] values() default {};

    @Target({ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        IdentifyFieldValue[] value();
    }

}

Hibernate Validator 自定義注解需要三個屬性,可參考jar包里面內置的注解實現(xiàn)

  • message 提示信息,可寫死,比如手機號類型,可直接定義為“非合法的手機號類型”,也可以在實現(xiàn)類驗證過程中自定義錯誤信息
  • groups 分組信息,這個屬性很重要!可以實現(xiàn)某個對象的屬性的校驗順序
  • payload 有效負載,使用者可以通過此屬性來給約束條件指定嚴重級別(不常使用)
    具體此注解的實現(xiàn)類實現(xiàn)由@Constraint(validatedBy = IdentifyFieldValueValidator.class)定義
    下面來看是實現(xiàn)類邏輯
/**
 * author:Java
 * Date:2020/5/28 15:35
 * 某個字段只能填寫固定的幾個值
 */
@Component
@Log4j2
public class IdentifyFieldValueValidator implements ConstraintValidator<IdentifyFieldValue, String> {

    private Class enumClass;
    private String[] values;

    @Override
    public void initialize(IdentifyFieldValue constraintAnnotation) {
        this.enumClass = constraintAnnotation.enumClass();
        this.values = constraintAnnotation.values();
    }

    @Override
    public boolean isValid(String objVal, ConstraintValidatorContext context) {
        if (StringUtils.isEmpty(objVal)) {
            return true;
        }

        String[] targetArr;
        //非某個注解類型
        if (enumClass == DefaultEnum.class) {
            targetArr = values;
        } else {
            //獲取某個注解里面所有的屬性值
            targetArr = getAllText(enumClass);
        }
        
        //判斷當前字段值是否在自定義的數(shù)組或者枚舉里
        String obj = Arrays.stream(targetArr).filter(it -> it.equals(objVal)).findAny().orElse(null);
        if (StringUtils.isEmpty(obj)) {
            //返回自定義提示消息
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("字段值只能為:[" + StringUtils.join(targetArr, ",") + "]").addConstraintViolation();
            return false;
        } else {
            return true;
        }
    }
    
    /**
     * 獲取某個枚舉類型下所有的text值
     * @param enumClass
     * @return
     */
    private static String[] getAllText(Class enumClass) {
        assert enumClass.isEnum();
        String[] arr = null;
        try {
            Enum[] enumConstants = (Enum[]) enumClass.getEnumConstants();
            arr = new String[enumConstants.length];
            //反射獲取枚舉類中的toString()方法
            Method method = enumClass.getMethod("toString");
            for (int i = 0; i < enumConstants.length; i++) {
                arr[i] = (String) method.invoke(enumConstants[i]);
            }
        } catch (Exception e) {
            log.error("IdentifyFieldValueValidator getAllText failed! enumClass:{}", enumClass.getName());
            e.printStackTrace();
        }
        return arr;
    }

}
//默認枚舉類
public enum  DefaultEnum {
}

在某個Bean屬性上增加該注解,還是以Student為例

@IdentifyFieldValue(enumClass = OrderType.class)
private String orderType;
@Getter
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum OrderType {

    /**
     * 保單類型
     */
    POLICY(0, "保單"),
    REVERSED_POLICY(1, "被沖正保單"),
    CORRECTION_POLICY(2, "沖正保單"),
    ENDORSEMENT(3, "批單"),
    REVERSED_ENDORSEMENT(4, "被沖正批單"),
    CORRECTION_ENDORSEMENT(5, "沖正批單"),
    CANCEL_POLICY(6, "退保"),
    ;


    private final int code;
    @EnumValue
    private final String text;
    OrderType(int code, String text) {
        this.code = code;
        this.text = text;
    }

    @JsonCreator
    public static OrderType get(int value) {
        return Arrays.stream(values()).filter(it -> it.getCode() == value).findAny().orElse(null);
    }

    public static OrderType get(String text) {
        return Arrays.stream(values()).filter(it -> it.getText().equals(text)).findAny().orElse(null);
    }

    @Override
    public String toString() {
        return text;
    }

}

測試類

Student student = new Student();
student.setOrderType("java is the best language in the world!")

//只校驗某個屬性
Set<ConstraintViolation<Student>> result2 = validator.validateProperty(student, "orderType");
printfError(result2);

輸出結果:
字段值只能為:[保單,被沖正保單,沖正保單,批單,被沖正批單,沖正批單,退保]
當然,這是在屬性上增加枚舉類型,如果是自定義的String[] values數(shù)組,也是可以的

屬性之間相互依賴---類級別校驗

有這樣一種場景,只有在Student對象中age>=18,salary字段必須大于100,這種單獨在某個屬性上增加注解的方式是無法解決的,下面介紹類級別約束

  • 注解類
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AgeSalaryTypeValidator.class)
@Documented
public @interface AgeSalaryType {

    String message() default "當年齡大于18歲時,每月薪水不得低于100元";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default { };

    @Target({ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        AgeSalaryType[] value();
    }

}
  • 校驗實現(xiàn)類
public class AgeSalaryTypeValidator implements ConstraintValidator<AgeSalaryType, Student> {
    @Override
    public boolean isValid(Student stu, ConstraintValidatorContext context) {
        //只有滿足age>= 18 并且 薪水低于100時,才會提示默認信息
        //當然這種是很簡單的業(yè)務場景,一旦業(yè)務邏輯變得復雜,比如需要根據當前對象的某幾個字段去查詢數(shù)據庫確定某個關系的時候,
        // 就顯得頗為受用了
        if (stu.getAge() >= 18 && stu.getSalary().compareTo(new BigDecimal(100)) < 0) {
            return false;
        }
        return true;
    }
}

注:實現(xiàn)類默認就是納入Sprin容器進行管理的,所以在實現(xiàn)類里面可以直接注入已經納入Spring容器管理的對象,這樣方便直接調取數(shù)據庫查詢

  • 實體類(將校驗注解直接加在類上面)
@Data
@AgeSalaryType
public class Student
  • 測試類
def testStudent() {
      Student student = new Student();
      student.setName("Java");
      student.setAge(20);
      student.setSalary(new BigDecimal(50));

      //整個對象完全校驗
      Set<ConstraintViolation<Student>> result = validator.validate(student);
      printfError(result);

      expect:
      true
    }
  • 測試結果
當年齡大于18歲時,每月薪水不得低于100元

這樣就可以解決類字段里面的依賴問題,我舉的例子是比較簡單的業(yè)務場景,邏輯簡單,大家可能覺得沒必要這么麻煩,但是如果邏輯復雜,比如我們現(xiàn)有邏輯,根據業(yè)務人員上傳的Excel表格明細,去確認數(shù)據庫中是否存在和表格中某幾個字段都相同的數(shù)據,如果存在,則打回,并追加提示該條數(shù)據已經上傳,具體的上傳時間,操作人等信息,這樣使用就是“真香”了吖

關于message

每個約束定義中都包含有一個用于提示驗證結果的消息模版, 并且在聲明一個約束條件的時候,你可以通過這個約束中的message屬性來重寫默認的消息模版, 如果在校驗的時候,這個約束條件沒有通過,那么你配置的MessageInterpolator會被用來當成解析器來解析這個約束中定義的消息模版, 從而得到最終的驗證失敗提示信息. 這個解析器會嘗試解析模版中的占位符( 大括號括起來的字符串 ). 其中, Hibernate Validator中默認的解析器 (MessageInterpolator) 會先在類路徑下找名稱為ValidationMessages.properties的ResourceBundle, 然后將占位符和這個文件中定義的resource進行匹配,如果匹配不成功的話,那么它會繼續(xù)匹配Hibernate Validator自帶的位于/org/hibernate/validator/ValidationMessages.properties的ResourceBundle, 依次類推,遞歸的匹配所有的占位符.

簡單理解,定義message幾種方式

  • @NotNull(message = '***不能為空'),主動聲明message
  • @NotNull, 如果不主動聲明,提示消息會默認jar包下的提示信息,位置如圖所示
    默認提示消息.jpg

    message是支持國際化的,所以也可以設置為中文
設置默認提示消息為中文

1.在application.yml配置文件下增加spring.messages.encoding=UTF-8

spring:
  messages:
    encoding: UTF-8

2.設置Idea關于properties文件的UTF-8編碼格式,如圖所示

idea下設置properties文件的UTF-8.jpg

其實就是讀取的上述圖里面的ValidationMessages_zh_CN.properties
3.當然,也是支持自定義的文本

@NotNull
@Size(min=5, max=16, message="{username.size}")
private String username;

在自己項目classpath下增加ValidationMessages_zh_CN.properties

classpath下增加文件

文件中的內容username.size = 用戶名長度在5-16之間
在校驗不通過是可提示此內容,至此,自定義提示消息就配置好了

關于group屬性的講解,比較重要,且聽下回分解


Java is the best language in the world
Tips:整理不易,如有轉載,請注明出處 http://www.itdecent.cn/p/5dcc50f332d1

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容