天天看點

Hibernate Validator實踐之一

在背景的業務邏輯中,對資料值的校驗在各層都存在(展示層,業務層,資料通路層等),并且各層校驗的規則又不盡相同,如下圖所示

Hibernate Validator實踐之一

注:該圖檔來自于Hibernate Validator官網

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

Hibernate Validator實踐之一

注:該圖檔來自于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'}]

繼續閱讀