建立自定義JSR303的驗證約束
為了建立一個自定義約束,以下三個步驟是必須的。
• Create a constraint annotation (首先定義一個約束註解)
• Implement a validator(第二步是實現這個驗證器)
• Define a default error message(最後新增一條預設的錯誤訊息即可)
假定有這麼一個要求,要驗證使用者的兩次輸入密碼必須是相同的,非常常見的一個要求。下面就基於這個要求來自定義一個約束。
Java程式碼- package org.leochen.samples;
- import javax.validation.Constraint;
- import javax.validation.Payload;
- import java.lang.annotation.*;
- /**
- * User: leochen
- * Date: 11-12-8
- * Time: 下午11:31
- */
-
@Target({ElementType.TYPE,ElementType.ANNOTATION_TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- @Constraint(validatedBy = MatchesValidator.class)
- @Documented
- public @interface Matches {
- String message() default "{constraint.not.matches}";
- Class<?>[] groups() default {};
- Class<? extends Payload>[] payload() default {};
-
String field();
- String verifyField();
- }
從上到下來說吧,@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程式碼- package org.leochen.samples;
- import org.apache.commons.beanutils.BeanUtils;
- import javax.validation.ConstraintValidator;
- import javax.validation.ConstraintValidatorContext;
- import java.lang.reflect.InvocationTargetException;
- /**
- * User: leochen
- * Date: 11-12-8
- * Time: 下午11:39
- */
- public class MatchesValidator implements ConstraintValidator<Matches,Object>{
- private String field;
- private String verifyField;
- public void initialize(Matches matches) {
- this.field = matches.field();
- this.verifyField = matches.verifyField();
- }
- public boolean isValid(Object value, ConstraintValidatorContext context) {
- try {
- String fieldValue= BeanUtils.getProperty(value,field);
- String verifyFieldValue = BeanUtils.getProperty(value,verifyField);
- boolean valid = (fieldValue == null) && (verifyFieldValue == null);
- if(valid){
- return true;
- }
- boolean match = (fieldValue!=null) && fieldValue.equals(verifyFieldValue);
- if(!match){
- String messageTemplate = context.getDefaultConstraintMessageTemplate();
- context.disableDefaultConstraintViolation();
- context.buildConstraintViolationWithTemplate(messageTemplate)
- .addNode(verifyField)
- .addConstraintViolation();
- }
- return match;
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- } catch (InvocationTargetException e) {
- e.printStackTrace();
- } catch (NoSuchMethodException e) {
- e.printStackTrace();
- }
- return true;
- }
- }
我們必須要實現ConstraintValidator這個介面,下面就來具體看看這個介面是怎麼定義的吧:
Java程式碼- package javax.validation;
- import java.lang.annotation.Annotation;
- public interface ConstraintValidator<A extends Annotation, T> {
- /**
- * Initialize the validator in preparation for isValid calls.
- * The constraint annotation for a given constraint declaration
- * is passed.
- * <p/>
- * This method is guaranteed to be called before any use of this instance for
- * validation.
- *
- * @param constraintAnnotation annotation instance for a given constraint declaration
- */
- void initialize(A constraintAnnotation);
- /**
- * Implement the validation logic.
- * The state of <code>value</code> must not be altered.
- *
- * This method can be accessed concurrently, thread-safety must be ensured
- * by the implementation.
- *
- * @param value object to validate
- * @param context context in which the constraint is evaluated
- *
- * @return false if <code>value</code> does not pass the constraint
- */
- boolean isValid(T value, ConstraintValidatorContext context);
- }
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方法中編寫驗證規則。
- String fieldValue= BeanUtils.getProperty(value,field);
- String verifyFieldValue = BeanUtils.getProperty(value,verifyField);
通過反射獲取驗證欄位的值,由於我們要實現的是一個密碼和確認密碼一致的問題,而這兩個欄位型別都是java.lang.String型別,所以我們直接通過BeanUtils來獲取他們各自的值。
Java程式碼- String messageTemplate = context.getDefaultConstraintMessageTemplate();
- context.disableDefaultConstraintViolation();
- context.buildConstraintViolationWithTemplate(messageTemplate)
- .addNode(verifyField)
- .addConstraintViolation();
以上是我們把驗證出錯的訊息放在哪個欄位上顯示,一般我們是在確認密碼上顯示密碼不一致的訊息。
好了,這樣我們的自定義約束就完成了,下面來使用並測試吧。
假如我們要驗證這麼一個formbean:
Java程式碼- package org.leochen.samples;
- /**
- * User: leochen
- * Date: 11-12-20
- * Time: 下午4:04
- */
- @Matches(field = "password", verifyField = "confirmPassword",
- message = "{constraint.confirmNewPassword.not.match.newPassword}")
- public class TwoPasswords {
- private String password;
- private String confirmPassword;
- public String getPassword() {
- return password;
- }
- public void setPassword(String password) {
- this.password = password;
- }
- public String getConfirmPassword() {
- return confirmPassword;
- }
- public void setConfirmPassword(String confirmPassword) {
- this.confirmPassword = confirmPassword;
- }
- }
在路徑下放入我們的資原始檔:ValidationMessages.properties(名字必須叫這個,不然你就費好大一番勁,何苦呢是不是,基於約定來)
Java程式碼- javax.validation.constraints.AssertFalse.message = must be false
- javax.validation.constraints.AssertTrue.message = must be true
- javax.validation.constraints.DecimalMax.message = must be less than or equal to {value}
- javax.validation.constraints.DecimalMin.message = must be greater than or equal to {value}
- javax.validation.constraints.Digits.message = numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)
- javax.validation.constraints.Future.message = must be in the future
- javax.validation.constraints.Max.message = must be less than or equal to {value}
- javax.validation.constraints.Min.message = must be greater than or equal to {value}
- javax.validation.constraints.NotNull.message = may not be null
- javax.validation.constraints.Null.message = must be null
- javax.validation.constraints.Past.message = must be in the past
- javax.validation.constraints.Pattern.message = must match "{regexp}"
- javax.validation.constraints.Size.message = size must be between {min} and {max}
- org.hibernate.validator.constraints.CreditCardNumber.message = invalid credit card number
- org.hibernate.validator.constraints.Email.message = not a well-formed email address
- org.hibernate.validator.constraints.Length.message = length must be between {min} and {max}
- org.hibernate.validator.constraints.NotBlank.message = may not be empty
- org.hibernate.validator.constraints.NotEmpty.message = may not be empty
- org.hibernate.validator.constraints.Range.message = must be between {min} and {max}
- org.hibernate.validator.constraints.SafeHtml.message = may have unsafe html content
- org.hibernate.validator.constraints.ScriptAssert.message = script expression "{script}" didn't evaluate to true
- org.hibernate.validator.constraints.URL.message = must be a valid URL
- ## custom constraints
- constraint.not.matches=two fields not matches
- constraint.confirmNewPassword.not.match.newPassword=two password not the same
單元測試如下:
Java程式碼- package org.leochen.samples;
- import org.junit.BeforeClass;
- import org.junit.Test;
- import javax.validation.ConstraintViolation;
- import javax.validation.Validation;
- import javax.validation.Validator;
- import javax.validation.ValidatorFactory;
- import java.util.Set;
- import static junit.framework.Assert.assertEquals;
- import static junit.framework.Assert.assertNotNull;
- /**
- * User: leochen <