在后台的業務邏輯中,對數據值的校驗在各層都存在(展示層,業務層,數據訪問層等),並且各層校驗的規則又不盡相同,如下圖所示
注:該圖片來自於Hibernate Validator官網
在各層中重復的校驗邏輯既導致了不必要的資源消耗,還使得邏輯不夠單一(每層都夾雜着校驗的邏輯),JSR 303 Bean Validation就是在這種背景下產生的一個數據驗證的J2EE規范。而我們這篇文中將要介紹的Hibernate Validator則是JBoss社區開源的一個JSR 303 Bean Validation規范的優秀實踐。
注:該圖片來自於Hibernate Validator官網
下面我們以一個具體的列子講述下如何在我們的工程中使用Hibernate Validator
首先我們定義了一個結構體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;
}
}
其中該類中用到Max,Min,NotNull,Size等都是JSR 303中內置的約束條件(constraint),GenderCase是自定義的約束條件,這個在后面會介紹。
對Bean進行約束校驗,首先需要先獲得一個校驗器實例ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
(一)如何對JSR 303 內置的約束條件進行校驗
根據上面Person結構體的定義,我們看個簡單的例子Person person = new Person(null, 20);
Set<ConstraintViolation<Person>> constraintViolations = validator.validate(person);
assertEquals(1, constraintViolations.size());
System.out.println(constraintViolations);
在Person結構體定義中,name不可以為null,這里我們故意構造了一個name的null的Person實例,結果在控制台輸入的結果如下:
[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'}]