以下內容轉載自:https://www.ibm.com/developerworks/cn/java/j-lo-beanvalid/
Bean Validation規范介紹
JSR303 規范(Bean Validation 規范)提供了對 Java EE 和 Java SE 中的 Java Bean 進行驗證的方式。該規范主要使用注解的方式來實現對 Java Bean 的驗證功能,並且這種方式會覆蓋使用 XML 形式的驗證描述符,從而使驗證邏輯從業務代碼中分離出來。javax.validation是JSR303規范,而hibernate-validator是則是規范的具體實現。
POM文件中引入hibernate-validator即可使用JSR303規范:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>4.3.0.Final</version> </dependency>
或者普通工程添加相關jar包:hibernate-validator 、jboss-logging 、 validation-api這些包吧.
入門案例:
先將最基本的Bean Validation封裝成一個簡單的工具類,方便使用,案例學習。
Validation API簡單封裝的ValidationUtils.java
public class ValidationUtils { public static Validator getValidator(){ return validator; } static Validator validator; static{ ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); validator=validatorFactory.getValidator(); } }
定義我們要校驗的Java Bean:
Employee.java類
@Setter @Getter @NoArgsConstructor //lombok注解 節約篇幅 代碼整潔 public class Employee { @NotNull(message = "員工姓名不能為空") @Size(min = 1,max = 10,message = "員工名字長度必須在10個字母以內") private String name; @NotNull(message = "員工ID不能為空") private Integer id; }
使用方式:
public static void main(String[] args) { Employee employee = new Employee(); employee.setName("lvbinbin6666"); Set<ConstraintViolation<Employee>> violations = ValidationUtils.getValidator().validate(employee); for (ConstraintViolation constraintViolation:violations) { System.out.println("當前有問題屬性為:"+constraintViolation.getPropertyPath().toString()); System.out.println("報錯原因為:"+constraintViolation.getMessage()); } }
查看結果,可以看到這就是Bean Validation的簡單使用,基於注解方式還是很方便的。
驗證流程
完成Java Bean的驗證流程通常分為四個步驟:
1.約束注解定義
2.約束驗證器驗證規則定義
3.約束注解聲明
4.約束驗證流程
自定義實現JSR303規范----@NotEmpty
Bean Vadalition中並沒有字符串為空的注解驗證,Hibernate-validator擴展了@NotEmpty來校驗 字符串不為空 ,自己實現一個字符串不為空的校驗規則
按照上面的流程,首先需要約束注解的定義,仿照@NotNull復制一個NotEmpty;
import static java.lang.annotation.ElementType.*; //靜態導包 @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) //注解應用的目標類型 @Retention(RetentionPolicy.RUNTIME) //注解應用時期 @Documented @Constraint(validatedBy = {NotEmptyValidator.class}) //注解關聯的驗證器,JSR303的注解 public @interface NotEmpty { String message() default ""; //驗證時輸出信息 Class<?>[] groups() default { }; //驗證時所屬的組別 Class<? extends Payload>[] payload() default {}; }
其中ValidatedBy就是自定義的驗證器的指向。第二步編寫自定義驗證器NotEmptyValidator
public class NotEmptyValidator implements ConstraintValidator<NotEmpty,String> { @Override public void initialize(NotEmpty constraintAnnotation) { } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if(value==null) return false; else if(value.trim().length()==0) return false; else return true; } }
第三步聲明約束注解(代碼為增量形式,重復的就忽略了)
@NotEmpty(message = "員工職位不能為空") private String job;
第四步驗證約束流程
public static void main(String[] args) { Employee employee = new Employee(); employee.setName(""); employee.setId(18); employee.setJob(""); Set<ConstraintViolation<Employee>> violations = ValidationUtils.getValidator().validate(employee); for (ConstraintViolation constraintViolation:violations) { System.out.println("當前有問題屬性為:"+constraintViolation.getPropertyPath().toString()); System.out.println("報錯原因為:"+constraintViolation.getMessage()); } }
驗證結果就不貼了,在自定義驗證器中輸出信息,發現確實調用了自定義的驗證規則;我們並沒有對@NotEmpty添加到Validator中,只是在實體類上聲明了,就會找到這個注解,進而根據Constraint注解上的validatedBy,找到自定義的驗證器,從而完成驗證流程!
Bean Validation內嵌的約束注解
非空性驗證兩個: @NotNull 不為空 ; @Null 為空
布爾型驗證兩個:@AssertTrue 布爾值為true ; @AssertFalse 布爾值不為空
日期類型驗證兩個:@Past 日期必須為過去的日期 ; @Future 日期必須為未來某個日期
正則表達式驗證一個:@Pattern
數值類型驗證若干:@Min String或Number類型大於等於該值 ; @Max String或Number類型小於等於該值 這兩種類型的值不支持小數校驗
@DecimalMin String或Number類型大於等於該值,支持小數 ; @DecimalMax String或Number類型小於等於該值,支持小數
集合驗證類型一個:@Size String、Array、Collection、Map長度類型在設定范圍內
多值約束問題
Bean Validation的一個特性:多值約束。
@NotNull等內嵌注解最下面都會有@interface List這樣一個注解,就是用來實現多值約束。 補充說明:一個注解無法再同一個位置標注兩次,編譯報錯Duplicate Annotaion,所以就有了內部注解,此外還可以通過@Repeatable容器的概念來實現多個注解標注。
舉個栗子,我們需要判斷一個字符串包含多個子串,類似地一個數組、集合、map包含這種屬性也是一樣的;
按照上面流程,定義約束注解 @Dictionary
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {DictionaryValidator.class}) public @interface Dictionary { String skillValue(); String message() default ""; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default {}; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) @Documented @interface List { Dictionary[] value(); } }
自定義約束驗證器規則編寫:
public class DictionaryValidator implements ConstraintValidator<Dictionary,String> { String skillValue=null; @Override public void initialize(Dictionary constraintAnnotation) { skillValue=constraintAnnotation.skillValue(); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if(value==null){ return false; } else if(value.trim().length()==0){ return false; }else if(value.contains(skillValue)){ return true; } return false; } }
第三步聲明注解,和@Repeatable有異曲同工之妙
@Dictionary.List(value = { @Dictionary(message = "該員工不會java",skillValue = "java"), @Dictionary(message = "該員工不會vue",skillValue = "vue") }) private String skill;
第四步 驗證約束
public static void main(String[] args) { Employee employee = new Employee(); employee.setName("lvbinbin"); employee.setId(18); employee.setJob(""); employee.setSkill("java"); Set<ConstraintViolation<Employee>> violations = ValidationUtils.getValidator().validate(employee); for (ConstraintViolation constraintViolation:violations) { System.out.println("當前有問題屬性為:"+constraintViolation.getPropertyPath().toString()); System.out.println("報錯原因為:"+constraintViolation.getMessage()); } }
得出結論:達到了我們的目的,校驗skill屬性包含 java vue子字符串,其實@Pattern編寫正則表達式就可以達到這樣的目的,但是集合 數組等類型多值判斷時就需要多值校驗了,還是有用武之處的!
組合約束
假設現在校驗員工身高,正常身高假設在1米以上3米以下,@Min(value=100) @Max(value=300)可能就完成這樣一個簡單的校驗規則。Bean Validation新特性,組合校驗。
import static java.lang.annotation.ElementType.*; @NotNull(message = "如實稟報身高") @Min(message = "正常人身高在1米以上",value = 100) @Max(message = "正常人身高在3米以下",value = 300) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) //注解應用的目標類型 @Retention(RetentionPolicy.RUNTIME) //注解應用時期 @Documented @Constraint(validatedBy = {}) public @interface Height { String message() default ""; //驗證時輸出信息 Class<?>[] groups() default { }; //驗證時所屬的組別 Class<? extends Payload>[] payload() default {}; }
聲明注解@Height
@Height private Integer height;
校驗流程
public static void main(String[] args) { Employee employee = new Employee(); employee.setName("lvbinbin"); employee.setId(18); employee.setJob(""); employee.setSkill("java vue"); employee.setHeight(183); Set<ConstraintViolation<Employee>> violations = ValidationUtils.getValidator().validate(employee); for (ConstraintViolation constraintViolation:violations) { System.out.println("當前有問題屬性為:"+constraintViolation.getPropertyPath().toString()); System.out.println("報錯原因為:"+constraintViolation.getMessage()); } }
補充記錄:測試時候發現,@Height注解報錯提示信息沒有用,因為組合的注解沒有通過,不是@Height注解校驗不通過,可以再自定義約束驗證器,這樣校驗不通過就能拋出@height中的message;
Bean Validation驗證規則流程
調用 Validator.validate(beanInstance) 方法后,Bean Validation 會查找在 beanInstance上所有的約束聲明(注解式),對每一個約束調用對應的約束驗證器進行驗證,由約束驗證器的 isValid 方法產生,如果該方法返回 true,則約束驗證成功,否則驗證失敗。驗證失敗生成約束違規對象(ConstraintViolation 的實例)並放到約束違規列表中。驗證完成后所有的驗證失敗信息均能在該列表中查找並輸出。
條件:靜態方法、字段無法約束驗證;可以在類或者接口上使用約束驗證,它將對該類或實現該接口的實例進行狀態驗證;
級聯驗證方式
對象之間存在級聯關系,A類存在屬性B,對A校驗的同時,如果需要對B完成校驗,在B上添加@Valid即可;
舉個栗子,假設Address存在屬性Employee,在對Address實例校驗時,在Employee上標注@Valid;
@Setter @Getter public class Address { @Valid Employee employee; @NotNull String location; }
校驗流程
public static void main(String[] args) { Employee employee = new Employee(); employee.setName("lvbinbin"); employee.setId(18); employee.setJob(""); employee.setSkill("java vue"); employee.setHeight(95); Address address = new Address(); address.setEmployee(employee); Set<ConstraintViolation<Address>> violations = ValidationUtils.getValidator().validate(address); for (ConstraintViolation constraintViolation:violations) { System.out.println("當前有問題屬性為:"+constraintViolation.getPropertyPath().toString()); System.out.println("報錯原因為:"+constraintViolation.getMessage()); } }
輸出結果: 注釋掉@Valid,employee屬性就不會校驗
Bean Validation注解生效
Bean Validation的注解可以在屬性上 、在getter方法上生效,至於為什么在setter方法上沒法生效 這個可能要成為謎題了;
測試getter上生效
private Date birth; public void setBirth(String date) throws ParseException { this.birth=new SimpleDateFormat("yyyy/MM/dd").parse(date); } @Past public Date getBirth(){ return birth; }
驗證流程:
public static void main(String[] args) throws ParseException { Employee employee = new Employee(); employee.setName("lvbinbin"); employee.setId(18); employee.setJob(""); employee.setSkill("java vue"); employee.setHeight(183); employee.setBirth("9102/12/31"); Set<ConstraintViolation<Employee>> violations = ValidationUtils.getValidator().validate(employee); for (ConstraintViolation constraintViolation:violations) { System.out.println("當前有問題屬性為:"+constraintViolation.getPropertyPath().toString()); System.out.println("報錯原因為:"+constraintViolation.getMessage()); } }
驗證結果:
組
Bean Validation中提出了組的概念,這點和Jackson中的@JsonView很類似,我只展示我需要的屬性;而Bean Validation中組,給每個校驗屬性分組,我只校驗我指定的組中包含的屬性,我不顯示的指定組名,那就校驗默認的組,Default組;
使用說明一:默認不指定組的話,下面Validator.validate(beanInstance)和Validator.validate(beanInstance,Default.class)這兩個輸出是一樣的,證明了默認組別叫Default; 這里的Default是javax.validation包中的,其他很多地方有這個類,不要到錯了!
@Setter @Getter public class User { @NotNull private String groupfieldA1; @NotNull private String groupfieldA2; @NotNull private String groupfieldB1; @NotNull private String groupfieldB2; public static void main(String[] args) { User user = new User(); //Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user); Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user, Default.class); for (ConstraintViolation constraintViolation:violations) { System.out.println("當前有問題屬性為:"+constraintViolation.getPropertyPath().toString()); System.out.println("報錯原因為:"+constraintViolation.getMessage()); } } }
使用說明二:
組別一定要采用接口定義,組別不是接口會拋出異常:javax.validation.ValidationException: HV000045: A group has to be an interface
組的接口不一定要重新再定義,哪怕使用已有的接口也沒有問題,我覺得只是作為標識來使用;
public static interface groupA{} @NotNull(groups = {groupA.class}) private String groupfieldA1;
這樣通過Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user, groupA.class)校驗,就只會針對groupA組的屬性校驗,即屬性groupfieldA1
使用說明三:
組接口繼承,屬性也會繼承被校驗; 這點類比JsonView
public static interface groupA{} public static interface groupB extends groupA{} @NotNull(groups = {groupA.class}) private String groupfieldA1; @NotNull(groups = {groupB.class}) private String groupfieldA2;
這樣通過Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user, groupB.class)校驗,組別B的屬性校驗,組別A的屬性也會被校驗;
組序列 @GroupSequence
Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user, groupA.class,groupB.class)支持多個組進行校驗,而且組與組之間互不影響,校驗組別順序只與輸入的組別順序級groupA>groupB有關,組序列為了滿足組與組之間互相存在影響而出現,保證了順序以及避免不必要的校驗(加入校驗B依賴於校驗A,校驗A都沒通過,校驗B就沒必要校驗了)
@Setter @Getter public class User { public static interface groupA{} public static interface groupB{} @GroupSequence(value = {groupA.class,groupB.class}) public static interface group {} @NotNull(groups = {groupA.class}) private String groupfieldA1; @NotNull(groups = {groupB.class}) private String groupfieldA2; @NotNull private String groupfieldB1; @NotNull private String groupfieldB2; public static void main(String[] args) { User user = new User(); //Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user); Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user, group.class); for (ConstraintViolation constraintViolation:violations) { System.out.println("當前有問題屬性為:"+constraintViolation.getPropertyPath().toString()); System.out.println("報錯原因為:"+constraintViolation.getMessage()); } } }
輸出截圖: @GroupSequence保證了校驗的順序,以及存在依賴關系時的校驗,第一個都沒有通過,后續不再校驗
Bean Validation接口規范
MessageInterpolator接口:消息解析器,用來將驗證過程中失敗的消息以可讀的方式傳遞給調用者; Bean Validation規范提供一個默認實現,configuration.getDefaultMessageInterpolator();
用戶自定義消息解析器只需要實現MessageInterpolator接口;
Bean Validation 規范的輸出消息默認從類路徑下的 ValidationMessage.properties 文件中讀取,用戶也可以在約束注解聲明的時候使用 message 屬性指定消息內容。
Configuration接口:收集上下文環境中的配置信息,主要用來計算如何給定正確的 ValidationProvider,並將其委派給 ValidatorFactory 對象。
參考文檔
BeanValidation : https://www.ibm.com/developerworks/cn/java/j-lo-beanvalid/