需求背景
最近在做和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位
內置的校驗注解

具體注解如何使用請查看源碼注釋,養(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)有需求,需要自定義注解
- 番外篇,關于Java的注解,可以看下Java注解完全解析
自定義注解
- 創(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編碼格式,如圖所示

其實就是讀取的上述圖里面的
ValidationMessages_zh_CN.properties3.當然,也是支持自定義的文本
@NotNull
@Size(min=5, max=16, message="{username.size}")
private String username;
在自己項目classpath下增加ValidationMessages_zh_CN.properties

文件中的內容
username.size = 用戶名長度在5-16之間在校驗不通過是可提示此內容,至此,自定義提示消息就配置好了
關于group屬性的講解,比較重要,且聽下回分解
Java is the best language in the world
Tips:整理不易,如有轉載,請注明出處 http://www.itdecent.cn/p/5dcc50f332d1
