1. 程式人生 > >建立自定義JSR303的驗證約束

建立自定義JSR303的驗證約束

        由於輸入驗證在軟體開發中是必須的一件事情,特別是與使用者互動的軟體產品,驗證使用者的潛在輸入錯誤是必不可少的一件事情,然而各種開源的驗證框架也很多,為了一統標準,jsr303規範橫空出世了,它定義了一些標準的驗證約束,標準畢竟是標準,它不可能定義到所有的驗證約束,它只是提供了一些基本的常用的約束,不過它提供了一個可拓展的自定義驗證約束。下面就來說說怎麼樣自定義一個約束.

      為了建立一個自定義約束,以下三個步驟是必須的。
      • Create a constraint annotation (首先定義一個約束註解)
      • Implement a validator(第二步是實現這個驗證器)
      • Define a default error message(最後新增一條預設的錯誤訊息即可)

    假定有這麼一個要求,要驗證使用者的兩次輸入密碼必須是相同的,非常常見的一個要求。下面就基於這個要求來自定義一個約束。

Java程式碼  收藏程式碼
  1. package org.leochen.samples;  
  2. import javax.validation.Constraint;  
  3. import javax.validation.Payload;  
  4. import java.lang.annotation.*;  
  5. /** 
  6.  * User: leochen 
  7.  * Date: 11-12-8 
  8.  * Time: 下午11:31 
  9.  */  
  10. @Target({ElementType.TYPE,ElementType.ANNOTATION_TYPE})  
  11. @Retention(RetentionPolicy.RUNTIME)  
  12. @Constraint(validatedBy = MatchesValidator.class)  
  13. @Documented  
  14. public @interface Matches {  
  15.     String message() default "{constraint.not.matches}";  
  16.     Class<?>[] groups() default {};  
  17.     Class<? extends Payload>[] payload() default {};  
  18.     String field();  
  19.     String verifyField();  
  20. }  

       從上到下來說吧,@Target表示註解可出現在哪些地方,比如可以出現在class上,field,method,又或者是在另外一個annotation上,這裡限制只能出現在類和另外一個註解上,@Retention表示該註解的儲存範圍是哪裡,RUNTIME表示在原始碼(source)、編譯好的.class檔案中保留資訊,在執行的時候會把這一些資訊載入到JVM中去的.@Constraint比較重要,表示哪個驗證器提供驗證。@interface表明這是一個註解,和class一樣都是關鍵字,message(),groups()和payload()這三個方法是一個標準的約束所具備的,其中message()是必須的,{constraint.not.matches}表示該訊息是要插值計算的,也就是說是要到資原始檔中尋找這個key的,如果不加{}就表示是一個普通的訊息,直接文字顯示,如果訊息中有需要用到{或}符號的,需要進行轉義,用\{和\}來表示。groups()表示該約束屬於哪個驗證組,在驗證某個bean部分屬性是特別有用(也說不清了,具體可以檢視Hibernate Validator的文件細看) default必須是一個型別為Class<?>[]的空陣列,attribute payload that can be used by clients of the Bean Validation API to assign custom payload objects to a constraint. This attribute is not used by the API itself.下面連個欄位是我們新增進去的,表示要驗證欄位的名稱,比如password和confirmPassword.

    下面就來實現這個約束。

Java程式碼  收藏程式碼
  1. package org.leochen.samples;  
  2. import org.apache.commons.beanutils.BeanUtils;  
  3. import javax.validation.ConstraintValidator;  
  4. import javax.validation.ConstraintValidatorContext;  
  5. import java.lang.reflect.InvocationTargetException;  
  6. /** 
  7.  * User: leochen 
  8.  * Date: 11-12-8 
  9.  * Time: 下午11:39 
  10.  */  
  11. public class MatchesValidator implements ConstraintValidator<Matches,Object>{  
  12.     private String field;  
  13.     private String verifyField;  
  14.     public void initialize(Matches matches) {  
  15.         this.field = matches.field();  
  16.         this.verifyField = matches.verifyField();  
  17.     }  
  18.     public boolean isValid(Object value, ConstraintValidatorContext context) {  
  19.         try {  
  20.             String fieldValue= BeanUtils.getProperty(value,field);  
  21.             String verifyFieldValue = BeanUtils.getProperty(value,verifyField);  
  22.             boolean valid = (fieldValue == null) && (verifyFieldValue == null);  
  23.             if(valid){  
  24.                 return true;  
  25.             }  
  26.             boolean match = (fieldValue!=null) && fieldValue.equals(verifyFieldValue);  
  27.             if(!match){  
  28.                 String messageTemplate = context.getDefaultConstraintMessageTemplate();  
  29.                 context.disableDefaultConstraintViolation();  
  30.                 context.buildConstraintViolationWithTemplate(messageTemplate)  
  31.                         .addNode(verifyField)  
  32.                         .addConstraintViolation();  
  33.             }  
  34.             return match;  
  35.         } catch (IllegalAccessException e) {  
  36.             e.printStackTrace();  
  37.         } catch (InvocationTargetException e) {  
  38.             e.printStackTrace();  
  39.         } catch (NoSuchMethodException e) {  
  40.             e.printStackTrace();  
  41.         }  
  42.         return true;  
  43.     }  
  44. }  

 我們必須要實現ConstraintValidator這個介面,下面就來具體看看這個介面是怎麼定義的吧:

Java程式碼  收藏程式碼
  1. package javax.validation;  
  2. import java.lang.annotation.Annotation;  
  3. public interface ConstraintValidator<A extends Annotation, T> {  
  4.     /** 
  5.      * Initialize the validator in preparation for isValid calls. 
  6.      * The constraint annotation for a given constraint declaration 
  7.      * is passed. 
  8.      * <p/> 
  9.      * This method is guaranteed to be called before any use of this instance for 
  10.      * validation. 
  11.      * 
  12.      * @param constraintAnnotation annotation instance for a given constraint declaration 
  13.      */  
  14.     void initialize(A constraintAnnotation);  
  15.     /** 
  16.      * Implement the validation logic. 
  17.      * The state of <code>value</code> must not be altered. 
  18.      * 
  19.      * This method can be accessed concurrently, thread-safety must be ensured 
  20.      * by the implementation. 
  21.      * 
  22.      * @param value object to validate 
  23.      * @param context context in which the constraint is evaluated 
  24.      * 
  25.      * @return false if <code>value</code> does not pass the constraint 
  26.      */  
  27.     boolean isValid(T value, ConstraintValidatorContext context);  
  28. }  

        A 表示邊界範圍為java.lang.annotation.Annotation即可,這個T引數必須滿足下面兩個限制條件:

  • T must resolve to a non parameterized type (T 必須能被解析為非引數化的型別,通俗講就是要能解析成具體型別,比如Object,Dog,Cat之類的,不能是一個佔位符)
  • or generic parameters of T must be unbounded wildcard types(或者也可以是一個無邊界範圍含有萬用字元的泛型型別)

        我們在(A constraintAnnotation) 方法中獲取到要驗證的兩個欄位的名稱,在isValid方法中編寫驗證規則。

Java程式碼  收藏程式碼
  1. String fieldValue= BeanUtils.getProperty(value,field);  
  2. String verifyFieldValue = BeanUtils.getProperty(value,verifyField);  

        通過反射獲取驗證欄位的值,由於我們要實現的是一個密碼和確認密碼一致的問題,而這兩個欄位型別都是java.lang.String型別,所以我們直接通過BeanUtils來獲取他們各自的值。

Java程式碼  收藏程式碼
  1. String messageTemplate = context.getDefaultConstraintMessageTemplate();  
  2.                 context.disableDefaultConstraintViolation();  
  3.                 context.buildConstraintViolationWithTemplate(messageTemplate)  
  4.                         .addNode(verifyField)  
  5.                         .addConstraintViolation();  

        以上是我們把驗證出錯的訊息放在哪個欄位上顯示,一般我們是在確認密碼上顯示密碼不一致的訊息。

        好了,這樣我們的自定義約束就完成了,下面來使用並測試吧。

       假如我們要驗證這麼一個formbean:

Java程式碼  收藏程式碼
  1. package org.leochen.samples;  
  2. /** 
  3.  * User: leochen 
  4.  * Date: 11-12-20 
  5.  * Time: 下午4:04 
  6.  */  
  7. @Matches(field = "password", verifyField = "confirmPassword",  
  8.                  message = "{constraint.confirmNewPassword.not.match.newPassword}")  
  9. public class TwoPasswords {  
  10.     private String password;  
  11.     private String confirmPassword;  
  12.     public String getPassword() {  
  13.         return password;  
  14.     }  
  15.     public void setPassword(String password) {  
  16.         this.password = password;  
  17.     }  
  18.     public String getConfirmPassword() {  
  19.         return confirmPassword;  
  20.     }  
  21.     public void setConfirmPassword(String confirmPassword) {  
  22.         this.confirmPassword = confirmPassword;  
  23.     }  
  24. }    

 在路徑下放入我們的資原始檔:ValidationMessages.properties(名字必須叫這個,不然你就費好大一番勁,何苦呢是不是,基於約定來)

Java程式碼  收藏程式碼
  1. javax.validation.constraints.AssertFalse.message = must be false  
  2. javax.validation.constraints.AssertTrue.message  = must be true  
  3. javax.validation.constraints.DecimalMax.message  = must be less than or equal to {value}  
  4. javax.validation.constraints.DecimalMin.message  = must be greater than or equal to {value}  
  5. javax.validation.constraints.Digits.message      = numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)  
  6. javax.validation.constraints.Future.message      = must be in the future  
  7. javax.validation.constraints.Max.message         = must be less than or equal to {value}  
  8. javax.validation.constraints.Min.message         = must be greater than or equal to {value}  
  9. javax.validation.constraints.NotNull.message     = may not be null  
  10. javax.validation.constraints.Null.message        = must be null  
  11. javax.validation.constraints.Past.message        = must be in the past  
  12. javax.validation.constraints.Pattern.message     = must match "{regexp}"  
  13. javax.validation.constraints.Size.message        = size must be between {min} and {max}  
  14. org.hibernate.validator.constraints.CreditCardNumber.message = invalid credit card number  
  15. org.hibernate.validator.constraints.Email.message            = not a well-formed email address  
  16. org.hibernate.validator.constraints.Length.message           = length must be between {min} and {max}  
  17. org.hibernate.validator.constraints.NotBlank.message         = may not be empty  
  18. org.hibernate.validator.constraints.NotEmpty.message         = may not be empty  
  19. org.hibernate.validator.constraints.Range.message            = must be between {min} and {max}  
  20. org.hibernate.validator.constraints.SafeHtml.message         = may have unsafe html content  
  21. org.hibernate.validator.constraints.ScriptAssert.message     = script expression "{script}" didn't evaluate to true  
  22. org.hibernate.validator.constraints.URL.message              = must be a valid URL  
  23. ## custom constraints  
  24. constraint.not.matches=two fields not matches  
  25. constraint.confirmNewPassword.not.match.newPassword=two password not the same  

 單元測試如下:

Java程式碼  收藏程式碼
  1. package org.leochen.samples;  
  2. import org.junit.BeforeClass;  
  3. import org.junit.Test;  
  4. import javax.validation.ConstraintViolation;  
  5. import javax.validation.Validation;  
  6. import javax.validation.Validator;  
  7. import javax.validation.ValidatorFactory;  
  8. import java.util.Set;  
  9. import static junit.framework.Assert.assertEquals;  
  10. import static junit.framework.Assert.assertNotNull;  
  11. /** 
  12.  * User: leochen <