Hibernate Validator--創建自己的約束規則


盡管Bean Validation API定義了一大堆標准的約束條件, 但是肯定還是有這些約束不能滿足我們需求的時候, 在這種情況下, 你可以根據你的特定的校驗需求來創建自己的約束條件.

3.1. 創建一個簡單的約束條件

按照以下三個步驟來創建一個自定義的約束條件

  • 創建約束標注

  • 實現一個驗證器

  • 定義默認的驗證錯誤信息

3.1.1. 約束標注

讓我們來創建一個新的用來判斷一個給定字符串是否全是大寫或者小寫字符的約束標注. 我們將稍后把它用在第 1 章 開始入門中的類CarlicensePlate字段上來確保這個字段的內容一直都是大寫字母.

首先,我們需要一種方法來表示這兩種模式( 譯注: 大寫或小寫), 我們可以使用String常量, 但是在Java 5中, 枚舉類型是個更好的選擇:

例 3.1. 枚舉類型CaseMode, 來表示大寫或小寫模式.

package com.mycompany; public enum CaseMode {     UPPER,      LOWER; }

現在我們可以來定義真正的約束標注了. 如果你以前沒有創建過標注(annotation)的話,那么這個可能看起來有點嚇人, 可是其實沒有那么難的 :)

例 3.2. 定義一個CheckCase的約束標注

package com.mycompany; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Target( { METHOD, FIELD, ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = CheckCaseValidator.class) @Documented public @interface CheckCase {     String message() default "{com.mycompany.constraints.checkcase}";     Class<?>[] groups() default {};     Class<? extends Payload>[] payload() default {};          CaseMode value(); }

一個標注(annotation) 是通過@interface關鍵字來定義的. 這個標注中的屬性是聲明成類似方法的樣式的. 根據Bean Validation API 規范的要求

  • message屬性, 這個屬性被用來定義默認得消息模版, 當這個約束條件被驗證失敗的時候,通過此屬性來輸出錯誤信息.

  • groups 屬性, 用於指定這個約束條件屬於哪(些)個校驗組(請參考第 2.3 節 “校驗組”). 這個的默認值必須是Class<?>類型到空到數組.

  • payload 屬性, Bean Validation API 的使用者可以通過此屬性來給約束條件指定嚴重級別. 這個屬性並不被API自身所使用.

    提示

    通過payload屬性來指定默認錯誤嚴重級別的示例

    public class Severity {
        public static class Info extends Payload {};
        public static class Error extends Payload {};
    }
    
    public class ContactDetails {
        @NotNull(message="Name is mandatory", payload=Severity.Error.class)
        private String name;
    
        @NotNull(message="Phone number not specified, but not mandatory", payload=Severity.Info.class)
        private String phoneNumber;
    
        // ...
    }

    這樣, 在校驗完一個ContactDetails 的示例之后, 你就可以通過調用ConstraintViolation.getConstraintDescriptor().getPayload()來得到之前指定到錯誤級別了,並且可以根據這個信息來決定接下來到行為.

除了這三個強制性要求的屬性(message, groups 和 payload) 之外, 我們還添加了一個屬性用來指定所要求到字符串模式. 此屬性的名稱value在annotation的定義中比較特殊, 如果只有這個屬性被賦值了的話, 那么, 在使用此annotation到時候可以忽略此屬性名稱, 即@CheckCase(CaseMode.UPPER).

另外, 我們還給這個annotation標注了一些(所謂的) 元標注( 譯注: 或"元模型信息"?, "meta annotatioins"):

  • @Target({ METHOD, FIELD, ANNOTATION_TYPE }): 表示@CheckCase 可以被用在方法, 字段或者annotation聲明上.

  • @Retention(RUNTIME): 表示這個標注信息是在運行期通過反射被讀取的.

  • @Constraint(validatedBy = CheckCaseValidator.class): 指明使用那個校驗器(類) 去校驗使用了此標注的元素.

  • @Documented: 表示在對使用了@CheckCase的類進行javadoc操作到時候, 這個標注會被添加到javadoc當中.

提示

Hibernate Validator provides support for the validation of method parameters using constraint annotations (see 第 8.3 節 “Method validation”).

In order to use a custom constraint for parameter validation the ElementType.PARAMETER must be specified within the @Target annotation. This is already the case for all constraints defined by the Bean Validation API and also the custom constraints provided by Hibernate Validator.

3.1.2. 約束校驗器

Next, we need to implement a constraint validator, that's able to validate elements with a @CheckCase annotation. To do so, we implement the interface ConstraintValidator as shown below:

例 3.3. 約束條件CheckCase的驗證器

package com.mycompany; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {     private CaseMode caseMode;     public void initialize(CheckCase constraintAnnotation) {         this.caseMode = constraintAnnotation.value();     }     public boolean isValid(String object, ConstraintValidatorContext constraintContext) {         if (object == null)             return true;         if (caseMode == CaseMode.UPPER)             return object.equals(object.toUpperCase());         else             return object.equals(object.toLowerCase());     } }

ConstraintValidator定義了兩個泛型參數, 第一個是這個校驗器所服務到標注類型(在我們的例子中即CheckCase), 第二個這個校驗器所支持到被校驗元素到類型 (即String).

如果一個約束標注支持多種類型到被校驗元素的話, 那么需要為每個所支持的類型定義一個ConstraintValidator,並且注冊到約束標注中.

這個驗證器的實現就很平常了, initialize() 方法傳進來一個所要驗證的標注類型的實例, 在本例中, 我們通過此實例來獲取其value屬性的值,並將其保存為CaseMode類型的成員變量供下一步使用.

isValid()是實現真正的校驗邏輯的地方, 判斷一個給定的String對於@CheckCase這個約束條件來說是否是合法的, 同時這還要取決於在initialize()中獲得的大小寫模式. 根據Bean Validation中所推薦的做法, 我們認為null是合法的值. 如果null對於這個元素來說是不合法的話,那么它應該使用@NotNull來標注.

3.1.2.1. ConstraintValidatorContext

例 3.3 “約束條件CheckCase的驗證器” 中的isValid使用了約束條件中定義的錯誤消息模板, 然后返回一個true 或者 false. 通過使用傳入的ConstraintValidatorContext對象, 我們還可以給約束條件中定義的錯誤信息模板來添加額外的信息或者完全創建一個新的錯誤信息模板.

例 3.4. 使用ConstraintValidatorContext來自定義錯誤信息

package com.mycompany; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {     private CaseMode caseMode;     public void initialize(CheckCase constraintAnnotation) {         this.caseMode = constraintAnnotation.value();     }     public boolean isValid(String object, ConstraintValidatorContext constraintContext) {         if (object == null)             return true;                  boolean isValid;         if (caseMode == CaseMode.UPPER) {             isValid = object.equals(object.toUpperCase());         }         else {             isValid = object.equals(object.toLowerCase());         }                  if(!isValid) {             constraintContext.disableDefaultConstraintViolation();             constraintContext.buildConstraintViolationWithTemplate( "{com.mycompany.constraints.CheckCase.message}"  ).addConstraintViolation();         }         return result;     } }

例 3.4 “使用ConstraintValidatorContext來自定義錯誤信息” 演示了如果創建一個新的錯誤信息模板來替換掉約束條件中定義的默認的. 在本例中, 實際上通過調用ConstraintValidatorContext達到了一個使用默認消息模板的效果.

提示

在創建新的constraint violation的時候一定要記得調用addConstraintViolation, 只有這樣, 這個新的constraint violation才會被真正的創建.

In case you are implementing a ConstraintValidator a class level constraint it is also possible to adjust set the property path for the created constraint violations. This is important for the case where you validate multiple properties of the class or even traverse the object graph. A custom property path creation could look like 例 3.5 “Adding new ConstraintViolation with custom property path”.

例 3.5. Adding new ConstraintViolation with custom property path

public boolean isValid(Group group, ConstraintValidatorContext constraintValidatorContext) {     boolean isValid = false;     ...     if(!isValid) {         constraintValidatorContext             .buildConstraintViolationWithTemplate( "{my.custom.template}" )             .addNode( "myProperty" ).addConstraintViolation();     }     return isValid; } 

3.1.3. 校驗錯誤信息

最后, 我們還需要指定如果@CheckCase這個約束條件驗證的時候,沒有通過的話的校驗錯誤信息. 我們可以添加下面的內容到我們項目自定義的ValidationMessages.properties (參考 第 2.2.4 節 “驗證失敗提示信息解析”)文件中.

例 3.6. 為CheckCase約束定義一個錯誤信息

com.mycompany.constraints.CheckCase.message=Case mode must be {value}.

如果發現校驗錯誤了的話, 你所使用的Bean Validation的實現會用我們定義在@CheckCase中message屬性上的值作為鍵到這個文件中去查找對應的錯誤信息.

3.1.4. 應用約束條件

現在我們已經有了一個自定義的約束條件了, 我們可以把它用在第 1 章 開始入門中的Car類上, 來校驗此類的licensePlate屬性的值是否全都是大寫字母.

例 3.7. 應用CheckCase約束條件

package com.mycompany; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; public class Car {     @NotNull     private String manufacturer;     @NotNull     @Size(min = 2, max = 14)     @CheckCase(CaseMode.UPPER)     private String licensePlate;     @Min(2)     private int seatCount;          public Car(String manufacturer, String licencePlate, int seatCount) {         this.manufacturer = manufacturer;         this.licensePlate = licencePlate;         this.seatCount = seatCount;     }     //getters and setters ... }

最后,讓我們用一個簡單的測試來檢測@CheckCase約束已經被正確的校驗了:

例 3.8. 演示CheckCase的驗證過程

package com.mycompany; import static org.junit.Assert.*; import java.util.Set; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; import org.junit.BeforeClass; import org.junit.Test; public class CarTest {     private static Validator validator;     @BeforeClass     public static void setUp() {         ValidatorFactory factory = Validation.buildDefaultValidatorFactory();         validator = factory.getValidator();     }     @Test     public void testLicensePlateNotUpperCase() {         Car car = new Car("Morris", "dd-ab-123", 4);         Set<ConstraintViolation<Car>> constraintViolations =             validator.validate(car);         assertEquals(1, constraintViolations.size());         assertEquals(             "Case mode must be UPPER.",              constraintViolations.iterator().next().getMessage());     }     @Test     public void carIsValid() {         Car car = new Car("Morris", "DD-AB-123", 4);         Set<ConstraintViolation<Car>> constraintViolations =             validator.validate(car);         assertEquals(0, constraintViolations.size());     } }

3.2. 約束條件組合

例 3.7 “應用CheckCase約束條件”中我們可以看到, 類CarlicensePlate屬性上定義了三個約束條件. 在某些復雜的場景中, 可能還會有更多的約束條件被定義到同一個元素上面, 這可能會讓代碼看起來有些復雜, 另外, 如果在另外的類里面還有一個licensePlate屬性, 我們可能還要把這些約束條件再拷貝到這個屬性上, 但是這樣做又違反了 DRY 原則.

這個問題可以通過使用組合約束條件來解決. 接下來讓我們來創建一個新的約束標注@ValidLicensePlate, 它組合了@NotNull, @Size@CheckCase:

例 3.9. 創建一個約束條件組合ValidLicensePlate

package com.mycompany; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @NotNull @Size(min = 2, max = 14) @CheckCase(CaseMode.UPPER) @Target( { METHOD, FIELD, ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = {}) @Documented public @interface ValidLicensePlate {     String message() default "{com.mycompany.constraints.validlicenseplate}";     Class<?>[] groups() default {};     Class<? extends Payload>[] payload() default {}; }

 

我們只需要把要組合的約束標注在這個新的類型上加以聲明 (注: 這正是我們為什么把annotation types作為了@CheckCase的一個target). 因為這個組合不需要額外的校驗器, 所以不需要聲明validator屬性.

現在, 在licensePlate屬性上使用這個新定義的"約束條件" (其實是個組合) 和之前在其上聲明那三個約束條件是一樣的效果了.

例 3.10. 使用ValidLicensePlate組合約束

package com.mycompany; public class Car {     @ValidLicensePlate     private String licensePlate;     //... }

 

The set of ConstraintViolations retrieved when validating a Car instance will contain an entry for each violated composing constraint of the @ValidLicensePlate constraint. If you rather prefer a single ConstraintViolation in case any of the composing constraints is violated, the @ReportAsSingleViolation meta constraint can be used as follows:

例 3.11. @ReportAsSingleViolation的用法

//... @ReportAsSingleViolation public @interface ValidLicensePlate {     String message() default "{com.mycompany.constraints.validlicenseplate}";     Class<?>[] groups() default {};     Class<? extends Payload>[] payload() default {}; }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM