基於Aviator的註解驅動驗證框架

gaarazhu發表於2012-08-25
        程式開發過程中,在同一系統中層層之間資料傳遞或者是異構系統之間同步非同步通訊的時候,我們經常需要對Java Bean進行屬性驗證,來決定是否繼續後續process,或者直接丟擲error message。
        傳統的做法,給每個驗證場景加一個驗證類,專門負責所有屬性的驗證,以及驗證結果的校驗和處理,這種做法最直白,但是不夠靈活。第二種就是Java 6自帶的驗證框架 Bean validation, 用註解的形式來表達屬性對應的約束,Bean validation在很大程度上提高了資料驗證的靈活性和複用性,但是第一,個人感覺擴充套件比較麻煩,首先你需要自定義個註解來代表對應的約束,其次你必須新建一個驗證器,最後你必須在註解類上做好兩者的mapping,對於每種約束,都逃不開這三步;第二點,Bean validation只能完成bean中單一屬性的驗證,不支援跨屬性驗證。
        第三種做法就是基於Aviator DIY的驗證框架EasyValidation,Aviator是淘寶開發的一款java表示式求值工具,感興趣的朋友可以google一下。為了靈活性,和Bean validation一樣,EasyValidation也是註解驅動的。我們認為所謂的約束其實就是一個結果為 true 或者 false 的表示式,所以首先把這些約束表示式加在屬性上面作為後設資料,然後通過反射去獲取表示式,最後利用aviator去求值,再對結果做處理。

首先構建約束表示式的註解:

@Target(FIELD)
@Retention(RUNTIME)
public @interface Validation {

public abstract String condition();
public abstract String errorMsg();

}


單個屬性支援多約束驗證:

@Target(FIELD)@Retention(RUNTIME)
public @interface Validations {

public abstract Validation[] values();

}


由於Aviator計算表示式之前會利用asm將表示式生成java位元組碼,並且支援cache,所以定義一個annotaion,加在javabean上可以表示是否需要對其屬性約束做cache:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExpressionCacheable {

}


核心驗證方法:

public static List<String> plainValidate(Object pojo, boolean validateAll) {


BeanUtilBeanAccess access = new BeanUtilBeanAccess();
List<Field> targetFields = new ArrayList<Field>();
List<String> msgList = new ArrayList<String>();
Map<String, Object> env = new HashMap<String, Object>();
boolean needCacheExpression = pojo.getClass().isAnnotationPresent(
ExpressionCacheable.class);


for (Field field : pojo.getClass().getDeclaredFields()) {
try {
String fieldName = field.getName();
Object value = access.get(fieldName, pojo);
env.put(fieldName, value);


if (field.isAnnotationPresent(Validations.class)
|| field.isAnnotationPresent(Validation.class)) {
targetFields.add(field);
}
} catch (Exception e) {
continue;
}
}


for (Field field : targetFields) {
for (Validation validation : getValidateAnnotations(field)) {
boolean validationPassed = true;


String expression = validation.condition();
Expression compiledExp = AviatorEvaluator.compile(expression,
needCacheExpression);
try {
validationPassed = (Boolean) compiledExp.execute(env);
} catch (Exception e) {
validationPassed = false;
}
if (!validationPassed) {
String errorMsg = validation.errorMsg();
if (errorMsg.contains("#".concat(field.getName()))) {
Object value = env.get(field.getName());
String parm = (null == value) ? null : value.toString();
errorMsg = errorMsg.replaceAll(
"#".concat(field.getName()), parm);
}
msgList.add(errorMsg);


if (!validateAll) {
return msgList;
} else {
continue;
}
}
}
}


return msgList;
}


大家可以看到,第一步會獲得java bean屬性名和屬性值,並且放到一個map中,這就是aviator的執行環境,EasyValidation之所以能支援跨屬性驗證,主要就是因為有這個env map,在約束表示式裡面可以直接使用屬性名,代表該屬性的值,aviator計算表示式的時候會把屬性名換成env map中對應的鍵值來進行計算。
對應的流程圖如下:


Aviator除了支援基本的運算表示式之外,還內嵌了一些常用的函式,比如string.contains, string.endswiths 等等,你可以直接在約束表示式裡使用他們,
看到這裡,可能你會問,如何自定義函式,很簡單!兩步!
比如我們想加一個函式,來判斷屬性值是否是一個合法的數值:

第一步,定義函式:



@Function
public class NumberValidFunction extends AbstractFunction {

@Override
public String getName() {
return "number.valid";
}


public AviatorObject call(Map<String, Object> env, AviatorObject arg1) {
String targetStr = FunctionUtils.getStringValue(arg1, env);
return AviatorBoolean.valueOf(CalculationUtils.isNumberic(targetStr));
}
}


直接擴充套件Aviator中的AbstractFunction,覆蓋getName()方法和call(...)方法,其中,name就是表示式中使用的函式名。
第二步,註冊函式:

AviatorEvaluator.addFunction(new NumberValidFunction());


這樣,該函式就可以使用了!好了,準備活動做好了,讓我們來看一個完整的例子吧,對Student物件我們有一些驗證,於是我們先把約束表示式加在屬性上面:

@ExpressionCacheable
public class Student{
    @Validation(condition = "string.notEmpty(name) && string.startsWith('gaara')", 
                                errorMsg = "Invalid name: #name")
    private String name;

    @Validation(condition = "age < fatherAge", errorMsg = "Invalid age")
    private int age;
    private int fatherAge;

     public int getAge(){
         return age;
     }

    public void setAge(int age){
        this.age = age;
     }

    public int getFatherAge(){
        return fatherAge;
     }

     public void setFatherAge(int fatherAge){
          this.fatherAge = fatherAge;
     }

    public String getName(){
       return name;
    }


    public void setName(String name){
       this.name = name;
    }


}


然後我們對一個Student例項進行驗證:

List<String> errorMsgs = ValidationUtils.plainValidate(
student, true);

一句話,就得到所有的errorMsg(第二個引數如果為false,返回第一個error message,不再繼續驗證)。


總結:1. 不要濫用cache。

            2. 自定義函式的註冊可以通過註解+掃描器在系統初始化的時候完成。

            3. EasyValidation中所有的驗證約束都是放在java類上,不支援XSD level的驗證約束,所以不支援EMS,因為EMS所需的 jar 一般都是用第三方控制元件通過XSD去生成,雖然你可以選擇生成原始碼再把約束append上去,但是每次更新schema都會讓你丟失原有的約束。

            4. 效能方面測試下來沒什麼問題,如果實在對反射不放心的話,可以試試別的方法,比如Unsafe類,但是前提是關掉編譯器對Restricted API的Errors/Warnings。

相關文章