Hibernate Validator 总结大全
背景
代碼開發過程中,參數的有效性校驗是一項很繁瑣的工作, 如果參數簡單,就那么幾個參數,直接通過ifelse可以搞定,如果參數太多,比如一個大對象有100多個字段作為入參,你如何校驗呢? 仍使用ifelse就是體力活了, Hibernate Validator 是很好的選擇。
官方文檔入口: https://hibernate.org/validator/
文章示例基于6.0版本,可以參考6.0的官方文檔:https://docs.jboss.org/hibernate/validator/6.0/reference/en-US/html_single/#validator-gettingstarted
掃碼查看原文:
maven依賴
Hibernate validator 依賴
<!-- hibernate validator --> <dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><version>6.0.13.Final</version> </dependency> <dependency><groupId>javax.el</groupId><artifactId>javax.el-api</artifactId><version>3.0.1-b06</version> </dependency> <dependency><groupId>org.glassfish.web</groupId><artifactId>javax.el</artifactId><version>2.2.6</version> </dependency>為了能讓示例代碼跑起來的一些必要依賴
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.8</version><scope>provided</scope> </dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13</version> </dependency>支持的校驗注解
javax.validation.constraints 包下面的校驗注解都支持,如下面這些注解,基本上見名知意, 就不一一解釋了
Max 最大值校驗 Min 最小值校驗 Range 范圍校驗,Min和Max的組合 NotBlank 不為空白字符的校驗 NotEmpty 數組、集合等不為空的校驗 NotNull 空指針校驗 Email 郵箱格式校驗 ....下面通過示例代碼來說明校驗器常用的幾種使用方式: 簡單對象校驗、分組校驗、
簡單對象校驗
建一個需要檢驗的參數類:
@Data public class SimpleBean {@NotBlank(message = "姓名不能為空")private String name;@NotNull(message = "年齡不能為空")@Range(min = 0, max = 100, message = "年齡必須在{min}和{max}之間")private Integer age;@NotNull(message = "是否已婚不能為空")private Boolean isMarried;@NotEmpty(message = "集合不能為空")private Collection collection;@NotEmpty(message = "數組不能為空")private String[] array;@Emailprivate String email;/*真實場景下面可能還有幾十個字段省略 ... ...*/}校驗測試
public class ValidateTest {//初始化一個校驗器工廠 private static ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure()//校驗失敗是否立即返回: true-遇到一個錯誤立即返回不在往下校驗,false-校驗完所有字段才返回.failFast(false).buildValidatorFactory();Validator validator = validatorFactory.getValidator();/*** 簡單對象校驗*/@Testpublic void testSimple() {SimpleBean s=new SimpleBean();s.setAge(5);s.setName(" ");s.setEmail("email");Set<ConstraintViolation<SimpleBean>> result=validator.validate(s);System.out.println("遍歷輸出錯誤信息:");//getPropertyPath() 獲取屬性全路徑名//getMessage() 獲取校驗后的錯誤提示信息result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage()));} }測試結果
遍歷輸出錯誤信息: email:不是一個合法的電子郵件地址 collection:集合不能為空 array:數組不能為空 name:姓名不能為空 isMarried:是否已婚不能為空嵌套對象校驗
嵌套對象
上面是簡單對象的校驗,我們來嘗試嵌套對象的校驗,類結構如下:
|--OrgBean |----EmployeeBean |------List<PersonBean>OrgBean.java代碼,對于嵌套對象校驗要注意, 需要在內部引用的對象上用到@Valid注解,否則不會校驗被引用對象的內部字段
@Data public class OrgBean {@NotNullprivate Integer id;@Valid //如果此處不用Valid注解,則不會去校驗EmployeeBean對象的內部字段 @NotNull(message = "employee不能為空")private EmployeeBean Employee; }EmployeeBean.java代碼
@Data public class EmployeeBean {@Valid@NotNull(message = "person不能為空")/*** 此處用到容器元素級別的約束: List<@Valid @NotNull PersonBean> * 會校驗容器內部元素是否為null,否則為null時會跳過校驗* NotNull注解的target包含ElementType.TYPE_USE,因此NotNull可以給泛型注解*/private List<@Valid @NotNull PersonBean> people; }PersonBean.java
@Data public class PersonBean {@NotBlank(message = "姓名不能為空")private String name;@NotNull(message = "年齡不能為空")@Range(min = 0, max = 100, message = "年齡必須在{min}和{max}之間")private Integer age;@NotNull(message = "是否已婚不能為空")private Boolean isMarried;@NotNull(message = "是否有小孩不能為空")private Boolean hasChild;@NotNull(message = "小孩個數不能為空")private Integer childCount;@NotNull(message = "是否單身不能為空")private Boolean isSingle;}校驗測試代碼
@Test public void testNested() {PersonBean p=new PersonBean();p.setAge(30);p.setName("zhangsan");//p.setIsMarried(true);PersonBean p2=new PersonBean();p2.setAge(30);//p2.setName("zhangsan2");p2.setIsMarried(false);//p2.setHasChild(true);OrgBean org=new OrgBean();//org.setId(1);List<PersonBean> list=new ArrayList<>();list.add(p);list.add(p2);//增加一個null,測試是否會校驗元素為nulllist.add(null);EmployeeBean e=new EmployeeBean();e.setPeople(list);org.setEmployee(e);Set<ConstraintViolation<OrgBean>> result=validator.validate(org);System.out.println("遍歷輸出錯誤信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage()));}測試結果
id:不能為null Employee.people[0].childCount:小孩個數不能為空 Employee.people[0].isSingle:是否單身不能為空 Employee.people[1].hasChild:是否有小孩不能為空 Employee.people[0].isMarried:是否已婚不能為空 Employee.people[1].name:姓名不能為空 Employee.people[1].childCount:小孩個數不能為空 Employee.people[2].<list element>:不能為null Employee.people[0].hasChild:是否有小孩不能為空 Employee.people[1].isSingle:是否單身不能為空結果分析:
(1)可以看到打印結果中校驗的屬性名有一長串: Employee.people[0].childCount
這是由于ConstraintViolation.getPropertyPath()函數返回的是屬性的全路徑名稱。
(2)還有List元素中的值為null也進行了校驗:Employee.people[2].:不能為null
這是因為使用了容器元素級別的校驗,這種校驗器可以使用在泛型參數里面,如注解在List元素的泛型里面增加@NotNull注解: private List<@Valid @NotNull PersonBean> people;
如果沒有該注解,則list.dd(null)添加的空指針元素不會被校驗。
Hibernate Validator 約束級別
(1)字段級別: 在字段上面添加校驗注解
本質上就是可以添加在字段上的注解,@Target({ElementType.FIELD})。
(2)屬性級別: 在方法上面添加注解,如注解在getName()方法上
本質上就是可以添加在方法上的注解,@Target({ElementType.METHOD}) 。
(3)容器級別:在容器里面添加注解
本質上就是可以添加在泛型上的注解,這個是java8新增的特性,@Target({ElementType.TYPE_USE})。
如這些類都可以支持容器級別的校驗:java.util.Iterable實現類,java.util.Map的key和values,java.util.Optional,java.util.OptionalInt,java.util.OptionalDouble,java.util.OptionalLong 等, 如:
List<@Valid @NotNull PersonBean> people;
private Map<@Valid Part, List<@Valid Manufacturer>> partManufacturers;
(4)類級別:添加在類上面的校驗注解
需要@Target({ElementType.TYPE})標注,當然如果有@Target({ElementType.TYPE_USE})也行,因為TYPE_USE包含TYPE。
分組校驗
有這樣一個需求:當People對象為已婚時(isMarried字段為true),需要校驗”配偶姓名“、”是否有小孩“等字段不能為空,當People對象為未婚時,需要校驗“是否單身”等其他字段不能為空, 這種需求可以通過分組檢驗來實現,將校驗邏輯分為兩個組,然后每次調用校驗接口時指定分組即可實現不同的校驗。 如果不管“是否已婚”都需要校驗的字段(如姓名、年齡這些字段等),則可以同時指定兩個分組。
靜態分組
靜態分組主要在類上面是使用GroupSequence注解指定一個或者多個分組,用于處理不同的校驗邏輯,我覺得這個基本上是寫死的不能更改,用不用分組區別不大,因此沒什么好說的,可以跳過直接看后面的動態分組。
@GroupSequence({ Group.UnMarried.class, Group.Married.class }) public class RentalCar extends PeopleBean {... ... }動態分組
“未婚”和“已婚”兩個分組的代碼如下,由于分組必須是一個Class,而且有沒有任何實現只是一個標記而已,因此我可以用接口。
public interface Group {//已婚情況的分組校驗interface Married {}//未婚情況的分組校驗interface UnMarried {}}校驗對象:People2Bean.java
@Data public class People2Bean {//不管是否已婚,都需要校驗的字段,groups里面指定兩個分組@NotBlank(message = "姓名不能為空",groups = {Group.UnMarried.class, Group.Married.class})private String name;@NotNull(message = "年齡不能為空",groups = {Group.UnMarried.class, Group.Married.class})@Range(min = 0, max = 100, message = "年齡必須在{min}和{max}之間",groups = {Group.UnMarried.class, Group.Married.class})private Integer age;@NotNull(message = "是否已婚不能為空",groups = {Group.UnMarried.class, Group.Married.class})private Boolean isMarried;//已婚需要校驗的字段@NotNull(message = "配偶姓名不能為空",groups = {Group.Married.class})private String spouseName;//已婚需要校驗的字段@NotNull(message = "是否有小孩不能為空",groups = {Group.Married.class})private Boolean hasChild;//未婚需要校驗的字段@NotNull(message = "是否單身不能為空",groups = {Group.UnMarried.class})private Boolean isSingle; }測試代碼:通過isMarried的值來動態指定分組校驗
@Test public void testGroup() {PeopleBean p=new PeopleBean();p.setAge(30);p.setName(" ");p.setIsMarried(false);Set<ConstraintViolation<PeopleBean>> result;//通過isMarried的值來動態指定分組校驗if(p.getIsMarried()){//如果已婚,則按照已婚的分組字段result=validator.validate(p, Group.Married.class);}else{//如果未婚,則只校驗未婚的分組字段result=validator.validate(p, Group.UnMarried.class);}System.out.println("遍歷輸出錯誤信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage())); }測試結果,可以發現,未婚校驗了isSingle字段,符合預期
遍歷輸出錯誤信息: name:姓名不能為空 isSingle:是否單身不能為空將上述代碼中的isMarried設置為true:p.setIsMarried(false) 再次執行結果如下,也是符合預期的
遍歷輸出錯誤信息: name:姓名不能為空 hasChild:是否有小孩不能為空 spouseName:配偶姓名動態分組優化
有沒有發現上面的分組校驗代碼實現不夠好?本來校驗我是要完全交給validator框架的,但是我還得在校驗框架之外面額外判斷isMarried再來決定校驗方式(如下代碼),這樣校驗代碼從校驗框架外泄了,不太優雅,有沒有優化的空間呢?
if(p.getIsMarried()){//如果已婚,則按照已婚的分組字段result=validator.validate(p, Group.Married.class); }else{//如果未婚,則只校驗未婚的分組字段result=validator.validate(p, Group.UnMarried.class); }其實通過DefaultGroupSequenceProvider接口可以優化,這才是真正的動態分組校驗,在該接口實現中判斷isMarried值,來實現動態設置分組,也就是將校驗的額外判斷邏輯從校驗框架外層轉移到了校驗框架中,外層業務代碼只需要調用校驗接口即可,而無需關注具體的校驗邏輯,這樣的框架才是優秀的。
如下PeopleGroupSequenceProvider.java類實現了DefaultGroupSequenceProvider接口
public class PeopleGroupSequenceProvider implements DefaultGroupSequenceProvider<People2Bean> {@Overridepublic List<Class<?>> getValidationGroups(People2Bean bean) {List<Class<?>> defaultGroupSequence = new ArrayList<>();// 這里必須將校驗對象的類加進來,否則沒有Default分組會拋異常,這個地方還沒太弄明白,后面有時間再研究一下 defaultGroupSequence.add(People2Bean.class);if (bean != null) {Boolean isMarried=bean.getIsMarried();///System.err.println("是否已婚:" + isMarried + ",執行對應校驗邏輯");if(isMarried!=null){if(isMarried){System.err.println("是否已婚:" + isMarried + ",groups: "+Group.Married.class);defaultGroupSequence.add(Group.Married.class);}else{System.err.println("是否已婚:" + isMarried + ",groups: "+Group.UnMarried.class);defaultGroupSequence.add(Group.UnMarried.class);}}else {System.err.println("isMarried is null");defaultGroupSequence.add(Group.Married.class);defaultGroupSequence.add(Group.UnMarried.class);}}else{System.err.println("bean is null");}return defaultGroupSequence;} }People2Bean.java類上要用到@GroupSequenceProvider注解指定一個GroupSequenceProvider
@GroupSequenceProvider(PeopleGroupSequenceProvider.class) public class People2Bean {//字段同上 //... ... }測試代碼
@Test public void testGroupSequence(){People2Bean p=new People2Bean();p.setAge(30);p.setName(" ");System.out.println("----已婚情況:");p.setIsMarried(true);Set<ConstraintViolation<People2Bean>> result=validator.validate(p);System.out.println("遍歷輸出錯誤信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage()));System.out.println("----未婚情況:");p.setIsMarried(false);result=validator.validate(p);System.out.println("遍歷輸出錯誤信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage()));}測試結果符合預期
----已婚情況: 遍歷輸出錯誤信息: name:姓名不能為空 spouseName:配偶姓名不能為空 hasChild:是否有小孩不能為空 ----未婚情況: 遍歷輸出錯誤信息: name:姓名不能為空 isSingle:是否單身不能為空自定義校驗器
Hibernate中有不少約束校驗器,但是不一定能滿足你的業務,因此它還支持自定義約束校驗器,一般是一個約束注解配合一個校驗器使用,校驗器需要實現ConstraintValidator接口,然后約束注解中通過`@Constraint(validatedBy = {ByteLengthValidator.class})綁定校驗器即可。 這里我寫三個示例來說明:
自定義枚舉校驗
在開發過程中,有很多參數類型限制只能使用某些枚舉值,我們可以通過自定義的校驗器來做約束,以最簡單的性別舉例,在我國性別只有男和女,校驗注解定義如下: EnumRange.java
@Documented @Constraint(//這個配置用于綁定校驗器:EnumRangeValidatorvalidatedBy = {EnumRangeValidator.class} ) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(EnumRange.List.class) public @interface EnumRange {//自定義默認的消息模板String message() default "枚舉值不正確,范圍如下:{}";//枚舉類,用于在校驗器中限定值的范圍Class<? extends Enum> enumType();//分組 Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)@Documented//支持數組校驗public @interface List {EnumRange[] value();} }校驗器類:EnumRangeValidator.java 實現 ConstraintValidator 接口, ConstraintValidator<EnumRange,String> 接口的第一個泛型參數綁定EnumRange注解,第二個參數綁定要校驗的值類型,這里是String。
public class EnumRangeValidator implements ConstraintValidator<EnumRange,String> {private Set<String> enumNames;private String enumNameStr;@Overridepublic void initialize(EnumRange constraintAnnotation) {Class<? extends Enum> enumType=constraintAnnotation.enumType();if(enumType==null){throw new IllegalArgumentException("EnumRange.enumType 不能為空");}try {//初始化:將枚舉值放到Set中,用于校驗Method valuesMethod = enumType.getMethod("values");Enum[] enums = (Enum[]) valuesMethod.invoke(null);enumNames = Stream.of(enums).map(Enum::name).collect(Collectors.toSet());enumNameStr = enumNames.stream().collect(Collectors.joining(","));} catch (Exception e) {throw new RuntimeException("EnumRangeValidator 初始化異常",e);}}@Overridepublic boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {if(value==null){return true;}boolean result = enumNames.contains(value);if(!result){//拿到枚舉中的message,并替換變量,這個變量是我自己約定的,//你在使用注解的message中有花括號,這里會被替換為用逗號隔開展示的枚舉值列表String message = constraintValidatorContext.getDefaultConstraintMessageTemplate().replace("{}",enumNameStr);//禁用默認值,否則會有兩條messageconstraintValidatorContext.disableDefaultConstraintViolation();//添加新的messageconstraintValidatorContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();}return result;} }我們來定義一個性別的枚舉:當然,你還可以用其他自定義枚舉,只要是枚舉值這個校驗就就能生效
public enum SexEnum {F("女"),M("男");String desc;SexEnum(String desc){this.desc=desc;}}被校驗的類:Person2Bean.java
@Data public class Person2Bean {@NotBlank(message = "姓名不能為空")private String name;@Range(min = 0, max = 100, message = "年齡必須在{min}和{max}之間")private Integer age;//性別用到上面的自定義注解,并指定枚舉類SexEnum,message模板里面約定變量綁定“{}” @EnumRange(enumType = SexEnum.class, message = "性別只能是如下值:{}")private String sex;}校驗測試代碼
@Test public void testSelfDef() {Person2Bean s=new Person2Bean();//性別設置為“A",校驗應該不通過 s.setSex("A");//s.setFriendNames(Stream.of("zhangsan","李四思").collect(Collectors.toList()));Set<ConstraintViolation<Person2Bean>> result=validator.validate(s);System.out.println("遍歷輸出錯誤信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage())); }校驗結果如下:性別設置為“A",校驗應該不通過不是枚舉值中的F和M,因此符合預期
遍歷輸出錯誤信息: sex:性別只能是如下值:F,M name:姓名不能為空自定義字節數校驗器
參數的字段值要存入數據庫,比如某個字段用的 Oracle 的 Varchar(4) 類型,那么該字段值的不能超過4個字節,一般可能會想到應用 @Length 來校驗,但是該校驗器校驗的是字符字符串長度,即用 String.length() 來校驗的,英文字母占用的字節數與String.length()一致沒有問題,但是中文不行,根據不同的字符編碼占用的字節數不一樣,比如一個中文字符用UTF8占用3個字節,用GBK占用兩個字節,而一個英文字符不管用的什么編碼始終只占用一個字節,因此我們來創建一個字節數校驗器。
校驗注解類:ByteMaxLength.java
@Documented //綁定校驗器:ByteMaxLengthValidator @Constraint(validatedBy = {ByteMaxLengthValidator.class}) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(ByteMaxLength.List.class) public @interface ByteMaxLength {//注意這里的max是指最大字節長度,而非字符個數,對應數據庫字段類型varchar(n)中的nint max() default Integer.MAX_VALUE;String charset() default "UTF-8";Class<?>[] groups() default {};String message() default "【${validatedValue}】的字節數已經超過最大值{max}";Class<? extends Payload>[] payload() default {};@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface List {ByteMaxLength[] value();} }校驗最大字節數的校驗器:ByteMaxLengthValidator.java ,注意里面約定了兩個綁定變量:chMax 和 enMax,分別對應中、英文的最大字符數,用于message模板中使得錯誤提示更加友好
public class ByteMaxLengthValidator implements ConstraintValidator<ByteMaxLength,String> {private int max;private Charset charset;@Overridepublic void initialize(ByteMaxLength constraintAnnotation) {max=constraintAnnotation.max();charset=Charset.forName(constraintAnnotation.charset());}@Overridepublic boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {if(value==null){return true;}int byteLength = value.getBytes(charset).length;//System.out.println("byteLength="+byteLength);boolean result = byteLength<=max;if(!result){//這里隨便用一個漢字取巧獲取每個中文字符占用該字符集的字節數int chBytes = "中".getBytes(charset).length;System.out.println("chBytes="+chBytes);//計算出最大中文字數int chMax = max/chBytes;//拿到枚舉中的message,并替換變量,這個變量是我自己約定的,//約定了兩個綁定變量:chMax 和 enMaxString message = constraintValidatorContext.getDefaultConstraintMessageTemplate().replace("{chMax}",String.valueOf(chMax)).replace("{enMax}",String.valueOf(max));//禁用默認值,否則會有兩條messageconstraintValidatorContext.disableDefaultConstraintViolation();//添加新的messageconstraintValidatorContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();}return result;} }校驗類
@Data public class Person2Bean {/*** message里面用到了前面約定的兩個變量:chMax和enMax,* 至于${validatedValue}是框架內置的變量,用于獲取當前被校驗對象的值*/@ByteMaxLength(max=4,charset = "UTF-8", message = "姓名【${validatedValue}】全中文字符不能超過{chMax}個字,全英文字符不能超過{enMax}個字母")private String name;/*** 該注解可以用于泛型參數:List<String> ,* 這樣可以校驗List中每一個String元素的字節數是否符合要求*/private List<@ByteMaxLength(max=4,charset = "UTF-8",message = "朋友姓名【${validatedValue}】的字節數不能超過{max}")String> friendNames;@Range(min = 0, max = 100, message = "年齡必須在{min}和{max}之間")private Integer age;//@EnumRange(enumType = SexEnum.class, message = "性別只能是如下值:{}")private String sex;}校驗測試代碼
@Test public void testSelfDef() {Person2Bean s=new Person2Bean();s.setName("張三");//s.setSex("M");s.setFriendNames(Stream.of("zhangsan","李四思","張").collect(Collectors.toList()));Set<ConstraintViolation<Person2Bean>> result=validator.validate(s);System.out.println("遍歷輸出錯誤信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage())); }運行結果,可以發現List中的元素也可以校驗
遍歷輸出錯誤信息: name:姓名【張三】全中文字符不能超過1個字,全英文字符不能超過4個字母 friendNames[0].<list element>:朋友姓名【zhangsan】的字節數不能超過4 friendNames[1].<list element>:朋友姓名【李四思】的字節數不能超過4由于上面用的UTF-8編碼,max=4,中文占三個字節,因此只能一個中文字符,換成GBK試一下
@ByteMaxLength(max=4,charset = "GBK", message = "姓名【${validatedValue}】全中文字符不能超過{chMax}個字,全英文字符不能超過{enMax}個字母") private String name;//可以用于校驗數組元素:List<String> private List<@ByteMaxLength(max=4,charset = "GBK",message = "朋友姓名【${validatedValue}】的字節數不能超過{max}")String> friendNames;同樣的測試代碼發現校驗結果不一樣了:name="張三"校驗通過了,由于GBK中文值占2個字節而不是3個字節
friendNames[1].<list element>:朋友姓名【李四思】的字節數不能超過4 friendNames[0].<list element>:朋友姓名【zhangsan】的字節數不能超過4自定義類級別的校驗器
類級別的校驗器沒什么特別的,無非是其可以注解到類上面,即由@Target({ElementType.TYPE})標注的注解。但是某些特殊場景非常有用,字段上的校驗器只能用于校驗單個字段,如果我們需要對多個字段進行特定邏輯的組合校驗就非常有用了。
下面的示例用于校驗:訂單價格==商品數量*商品價格
@OrderPrice注解:OrderPrice.java
@Documented //綁定校驗器 @Constraint(validatedBy = {OrderPriceValidator.class}) //可以發現沒有 ElementType.TYPE 該注解也能用到類上面,這是因為ElementType.TYPE_USE包含ElementType.TYPE @Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(OrderPrice.List.class) public @interface OrderPrice {Class<?>[] groups() default {};String message() default "訂單價格不符合校驗規則";Class<? extends Payload>[] payload() default {};@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface List {OrderPrice[] value();} }校驗器: OrderPriceValidator.java,注意ConstraintValidator<OrderPrice, OrderBean>第二個泛型參數為被校驗的類OrderBean
public class OrderPriceValidator implements ConstraintValidator<OrderPrice, OrderBean> {@Overridepublic void initialize(OrderPrice constraintAnnotation) {}@Overridepublic boolean isValid(OrderBean order, ConstraintValidatorContext constraintValidatorContext) {if(order==null){return true;}return order.getPrice()==order.getGoodsPrice()*order.getGoodsCount();}}被校驗類:OrderBean.java
@Data //類上面用到自定義的校驗注解 @OrderPrice public class OrderBean {@NotBlank(message = "商品名稱不能為空")private String goodsName;@NotNull(message = "商品價格不能為空")private Double goodsPrice;@NotNull(message = "商品數量不能為空")private Integer goodsCount;@NotNull(message = "訂單價格不能為空")private Double price;@NotBlank(message = "訂單備注不能為空")private String remark;}校驗測試代碼
@Test public void testSelfDef2() {OrderBean o=new OrderBean();o.setGoodsName("辣條");o.setGoodsCount(5);o.setGoodsPrice(1.5);o.setPrice(20.5);Set<ConstraintViolation<OrderBean>> result=validator.validate(o);System.out.println("遍歷輸出錯誤信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage())); }測試執行結果如下:符合預期
遍歷輸出錯誤信息: :訂單價格不符合校驗規則 remark:訂單備注不能為空EL表達式
其實在上面的示例中,可以看到在message中已經使用到了EL表達式:
@ByteMaxLength(max=4,charset = "GBK", message = "姓名【${validatedValue}】全中文字符不能超過{chMax}個字,全英文字符不能超過{enMax}個字母")private String name;包含在${與}之間的就是EL表達式,比如這里的${validatedValue} , validatedValue是內置的變量,用于存儲當前被校驗對象的值,更復雜的用法不僅僅是取值,還可以做各種邏輯運算、內置函數調用等,如下面這些用法:
@Size(min = 2,max = 14,message = "The license plate '${validatedValue}' must be between {min} and {max} characters long" )@Min(value = 2,message = "There must be at least {value} seat${value > 1 ? 's' : ''}" )DecimalMax(value = "350",message = "The top speed ${formatter.format('%1$.2f', validatedValue)} is higher than {value}" )@DecimalMax(value = "100000", message = "Price must not be higher than ${value}")上面有一種不包含$符號,只包含在花括號{}的表達式,這種表達式只能用于簡單的變量替換,如果沒有該變量也不會報錯,只是會被原樣輸出,而${validatedValue}這個里面的表達式如果錯了則會拋異常。
比如@Length注解有兩個變量min和max,其實像groups、payload都可以獲取到其值,也就是在message中可以獲取當前注解的所有成員變量值(除了message本身)。
public @interface Length {int min() default 0;int max() default 2147483647;String message() default "{org.hibernate.validator.constraints.Length.message}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};... ... }如:
@Length(min=1,max=10,message = "字符長度請控制在{min}到{max}之間,分組校驗:{groups},消息:{message}") private String name;上述代碼的message中{min}、{max}、{groups}最終在錯誤消息輸出時hi可以被對應的變量值替換的,但是{message}就會被原樣輸出,因為不可能在message里面獲取它自己的值。
校驗框架對EL表達式的支持對于自定義消息模板非常有用,可以使錯誤消息提示更加友好。
SpringMVC中如何使用
上面的示例代碼都是在單元測試中使用,validator類也是自己手動創建的,在spring中validator需要通過容器來創建,除了上面的maven依賴,還需在spring.xml中為校驗器配置工廠bean
<mvc:annotation-driven validator="validator"/> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"><property name="providerClass" value="org.hibernate.validator.HibernateValidator"/><property name="validationMessageSource" ref="messageSource"/> </bean><bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">然后在Controller類中方法的參數增加@Valid注解即可
@RequestMapping("/update") public String update(@Valid PersonBean person) {//TODO ... }總結
寫到這里,上面提到的validator框架用法基本能滿足我們大多數業務場景了,我是最近在為公司寫業務代碼過程中對各種繁瑣的校驗頭痛不已,前期都是直接用ifelse搞定,后面覺得干體力活沒意思,因此通過validator框架把公司代碼現有校驗邏輯重構了一遍,非常受用,重構時比較痛苦,但是后面再使用就非常輕松了,上面這些場景都是我真實用到的,因此在這里總結一下做個筆記。
所有代碼都在如下倉庫: github-validator
總結
以上是生活随笔為你收集整理的Hibernate Validator 总结大全的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 腾讯面试:我倒在了网络基础知识
- 下一篇: CMOS曝光时间、积分时间