在後臺的業務邏輯中,對數據值的校驗在各層都存在(展現層,業務層,數據訪問層等),而且各層校驗的規則又不盡相同,以下圖所示html

注:該圖片來自於Hibernate Validator官網java

在各層中重複的校驗邏輯既致使了沒必要要的資源消耗,還使得邏輯不夠單一(每層都夾雜着校驗的邏輯),JSR 303 Bean Validation就是在這種背景下產生的一個數據驗證的J2EE規範。而咱們這篇文中將要介紹的Hibernate Validator則是JBoss社區開源的一個JSR 303  Bean Validation規範的優秀實踐。git

注:該圖片來自於Hibernate Validator官網正則表達式

下面咱們以一個具體的列子講述下如何在咱們的工程中使用Hibernate Validatoride

首先咱們定義了一個結構體Person,具體的定義以下
public class Person {
@NotNull
private String name;
@Min(value = 1)
private int age;
@NotNull(groups = Intf1.class)
@Size(min = 1, max = 3, groups = Intf2.class)
private String group;
@GenderCase(value = GenderType.FEMALE)
private GenderType gender;
@Max(100)
public int getAge() {
return age;
}
@Max(50)
public int getAgeOther() {
return age + 2;
}
@Max(50)
public int getAgeOther(int num) {
return 51;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
post

其中該類中用到Max,Min,NotNull,Size等都是JSR 303中內置的約束條件(constraint),GenderCase是自定義的約束條件,這個在後面會介紹。ui

對Bean進行約束校驗,首先須要先得到一個校驗器實例
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
this

(一)如何對JSR 303 內置的約束條件進行校驗
根據上面Person結構體的定義,咱們看個簡單的例子
Person person = new Person(null, 20);
Set<ConstraintViolation<Person>> constraintViolations = validator.validate(person);
assertEquals(1, constraintViolations.size());
System.out.println(constraintViolations);
hibernate

在Person結構體定義中,name不能夠爲null,這裏咱們故意構造了一個name的null的Person實例,結果在控制檯輸入的結果以下:code

[ConstraintViolationImpl{interpolatedMessage='不能爲null', propertyPath=name, rootBeanClass=class hibernate.validator.Person, messageTemplate='{javax.validation.constraints.NotNull.message}'}]

從上面的例子能夠看出,只須要在定義結構體中將JSR 303 內置的約束註解添加到對應的屬性上,經過Validator實例的validate方法,若是返回的Set集合不爲空,經過遍歷集合即可知哪些屬性的值非法。

Bean Validation 中的 constraint
表 1. Bean Validation 中內置的 constraint
@Null 被註釋的元素必須爲 null
@NotNull 被註釋的元素必須不爲 null
@AssertTrue 被註釋的元素必須爲 true
@AssertFalse 被註釋的元素必須爲 false
@Min(value) 被註釋的元素必須是一個數字,其值必須大於等於指定的最小值
@Max(value) 被註釋的元素必須是一個數字,其值必須小於等於指定的最大值
@DecimalMin(value) 被註釋的元素必須是一個數字,其值必須大於等於指定的最小值
@DecimalMax(value) 被註釋的元素必須是一個數字,其值必須小於等於指定的最大值
@Size(max, min) 被註釋的元素的大小必須在指定的範圍內
@Digits (integer, fraction) 被註釋的元素必須是一個數字,其值必須在可接受的範圍內
@Past 被註釋的元素必須是一個過去的日期
@Future 被註釋的元素必須是一個未來的日期
@Pattern(value) 被註釋的元素必須符合指定的正則表達式
表 2. Hibernate Validator 附加的 constraint
@Email 被註釋的元素必須是電子郵箱地址
@Length 被註釋的字符串的大小必須在指定的範圍內
@NotEmpty 被註釋的字符串的必須非空
@Range 被註釋的元素必須在合適的範圍內

注:上面兩個表格中的內容來自於這裏
若是有結構體嵌套,只須要在複合屬性上經過Valid註解,則能夠遞歸的進行校驗。

(二)validateValue與validateProperty
經過javax.validation.Validator接口類的定義可知,校驗的方法有三個,分別是validate,validateProperty,validateValue。其中validate會將全部的屬性進行約束校驗,而validateProperty是針對某一個具體的屬性進行校驗,validateValue是對具體的某一個屬性和特定的值進行校驗。具體看下面的兩個例子

第一個例子:

Person person = new Person(null, 101);
Set<ConstraintViolation<Person>> constraintViolations = validator.validateProperty(person, "age");
assertEquals(1, constraintViolations.size());
System.out.println(constraintViolations);

根據上面的結構定義能夠看出來在Person結構體中對age的約束有兩個,一個是最小值爲1,另外一個是個getter方法上的約束最大不能超過100,執行上面的邏輯輸出的結果爲:

[ConstraintViolationImpl{interpolatedMessage='最大不能超過100', propertyPath=age, rootBeanClass=class hibernate.validator.Person, messageTemplate='{javax.validation.constraints.Max.message}'}]

可見validateProperty不光是對field的值進行校驗,還會對getter方法也進行校驗。

第二個例子:

Set<ConstraintViolation<Person>> constraintViolations = validator.validateValue(Person.class, "name", null);
assertEquals(1, constraintViolations.size());
System.out.println(constraintViolations);

第二個例子表示在執行validateValue時,給定一個結構體定義,field的名稱,看該特定的值是否符合約束,執行的結果以下:

[ConstraintViolationImpl{interpolatedMessage='不能爲null', propertyPath=name, rootBeanClass=class hibernate.validator.Person, messageTemplate='{javax.validation.constraints.NotNull.message}'}]

(三)約束條件的分組
在JSR 303 中定義了group的概念,用定義的接口類來標識,在上面的Person結構體定義的例子中能夠看出有個group屬性,該屬性上有兩個約束,分別是@NotNull(groups = Intf1.class) 和@Size(min = 1, max = 3, groups = Intf2.class)

下面經過一段代碼執行的結果來看在參數校驗中如何進行分組

Set<ConstraintViolation<Person>> constraintViolations = validator.validateValue(Person.class, "group", null, Intf1.class);
assertEquals(1, constraintViolations.size());
System.out.println("validate Intf1 |" + constraintViolations);
constraintViolations = validator.validateValue(Person.class, "group",null, Intf2.class);
assertEquals(0, constraintViolations.size());
System.out.println("validate Intf2 |" + constraintViolations);
constraintViolations = validator.validateValue(Person.class, "group","test", Intf2.class, Intf1.class);
assertEquals(1, constraintViolations.size());
System.out.println("validate Intf1&Intf2 |" + constraintViolations);

上段邏輯當group值爲null時,首先用Intf1標識的約束條件進行校驗,在用Intf2標識的約束條件進行校驗。當group值爲test時,同時用Intf1和Intf2標識的約束條件進行校驗。執行的結果以下:

validate Intf1 | [ConstraintViolationImpl{interpolatedMessage='不能爲null', propertyPath=group, rootBeanClass=class hibernate.validator.Person, messageTemplate='{javax.validation.constraints.NotNull.message}'}]
validate Intf2 | []
validate Intf1&Intf2 | [ConstraintViolationImpl{interpolatedMessage='個數必須在1和3之間', propertyPath=group, rootBeanClass=class hibernate.validator.Person, messageTemplate='{javax.validation.constraints.Size.message}'}]

從上面的例子能夠看出,能夠根據具體的業務選擇不一樣的校驗規則。

(四)約束條件的定製
對約束條件的定製,須要兩步,第一步是定義約束的註解類:

@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = GenderTypeValidator.class)
public @interface GenderCase {
String message() default "genderType invalid";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
GenderType value() default GenderType.FEMALE;
}

這裏須要關注的有三個點,第一個是@Constraint(validatedBy = GenderTypeValidator.class) 這裏指定了下面將要說的約束校驗的實現類,第二個是message屬性,用於校驗值非法是缺省的消息模版,第三個是註解對應的約束值value。在這個例子中,註解的約束值用的是一個枚舉值表示男/女,缺省值爲女

第二步是實現了javax.validation.ConstraintValidator<A extends Annotation, T>接口的約束校驗實現類,上面說的validatedBy指向的就是該實現類,其中A表示自定義的註解類,T表示進行校驗的字段的類型。具體的邏輯定於以下:

public class GenderTypeValidator implements
ConstraintValidator<GenderCase, GenderType> {
GenderType value;
@Override
public void initialize(GenderCase constraintAnnotation) {
value = constraintAnnotation.value();
}
@Override
public boolean isValid(GenderType obj, ConstraintValidatorContext context) {
if (value != null && obj != null && value != obj) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("gender should be " + value + "| the value is " + obj).addConstraintViolation();
return false;
} else {
return true;
}
}
}

在初始化方法中獲取該註解的約束條件,在isValid方法中將傳進來的obj的值與約束條件比較,若是知足則返回true表示校驗經過,若是不知足則返回false,並將錯誤信息存儲上上下文ConstraintValidatorContext中,最終反饋給調用者。

下面是調用該定製約束條件的邏輯:

Set<ConstraintViolation<Person>> constraintViolations = validator.validateValue(Person.class, "gender", GenderType.MALE);
assertEquals(1, constraintViolations.size());
System.out.println(constraintViolations);

執行的結果以下:

[ConstraintViolationImpl{interpolatedMessage='gender should be FEMALE| the value is MALE', propertyPath=gender, rootBeanClass=class hibernate.validator.Person, messageTemplate='gender should be FEMALE| the value is MALE'}]