基於Aviator的註解驅動驗證框架
程式開發過程中,在同一系統中層層之間資料傳遞或者是異構系統之間同步非同步通訊的時候,我們經常需要對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去求值,再對結果做處理。
單個屬性支援多約束驗證:
大家可以看到,第一步會獲得java bean屬性名和屬性值,並且放到一個map中,這就是aviator的執行環境,EasyValidation之所以能支援跨屬性驗證,主要就是因為有這個env map,在約束表示式裡面可以直接使用屬性名,代表該屬性的值,aviator計算表示式的時候會把屬性名換成env map中對應的鍵值來進行計算。
對應的流程圖如下:
Aviator除了支援基本的運算表示式之外,還內嵌了一些常用的函式,比如string.contains, string.endswiths 等等,你可以直接在約束表示式裡使用他們,
看到這裡,可能你會問,如何自定義函式,很簡單!兩步!
比如我們想加一個函式,來判斷屬性值是否是一個合法的數值:
直接擴充套件Aviator中的AbstractFunction,覆蓋getName()方法和call(...)方法,其中,name就是表示式中使用的函式名。
第二步,註冊函式:
傳統的做法,給每個驗證場景加一個驗證類,專門負責所有屬性的驗證,以及驗證結果的校驗和處理,這種做法最直白,但是不夠靈活。第二種就是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();
}
@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());
@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。
相關文章
- 基於事件驅動的測試框架ETS事件框架
- Spring Boot 基於註解驅動原始碼分析--自動配置Spring Boot原始碼
- Spring Boot 基於註解驅動原始碼分析--自動掃描Spring Boot原始碼
- 基於註解的6.0許可權動態請求框架——JPermission框架
- django專案基於鉤子驗證的註冊功能Django
- 基於 java 註解的 csv 檔案讀寫框架Java框架
- 基於Annotation註解整合SSH框架和基於XML檔案配置Bean整合SSH框架框架XMLBean
- 基於AOP的MVP框架(三)GoMVP進階註解MVP框架Go
- 基於AOP的MVP框架(二)GoMVP進階註解MVP框架Go
- 基於 TrueLicense 的專案證書驗證
- Reactor: Spring釋出基於JVM的非同步事件驅動框架ReactSpringJVM非同步事件框架
- 基於WDF的驅動開發
- 【Android】註解框架(二) 基礎知識(Java註解)& 執行時註解框架Android框架Java
- IPA加驅動的一種方式,未驗證
- 【Spring註解驅動開發】聊聊Spring註解驅動開發那些事兒!Spring
- 基於vue 做了關於token驗證的例項,和移動端下fixed失效的解決方案Vue
- Bevy:基於Rust的資料驅動遊戲引擎和應用程式框架Rust遊戲引擎框架
- 自定義基於XML的驗證器XML
- Apache基於MySQL的身份驗證(轉)ApacheMySql
- Asp.net中基於Forms驗證的角色驗證授權ASP.NETORM
- 5.3. 驗證 Seam安全中的驗證特性是基於JAAS
- 基於Python的介面自動化-unittest測試框架和ddt資料驅動Python框架
- 基於Linux的tty架構及UART驅動詳解Linux架構
- 基於註解的 Spring MVC詳解SpringMVC
- node實現基於token的身份驗證
- 【String註解驅動開發】你瞭解@PostConstruct註解和@PreDestroy註解嗎?Struct
- 關於谷歌賬號註冊手機號無法驗證的解決方法谷歌
- Solon詳解(六)- 定製業務級別的驗證註解
- 領域驅動設計實踐——驗證(一)
- 基於 Redis驅動的 Laravel 事件廣播RedisLaravel事件
- 基於測試驅動的iOS開發iOS
- 新字元驅動框架驅動LED字元框架
- Spring基於註解的IoC配置Spring
- Spring基於註解的aop配置Spring
- Spring(5、基於註解的事物)Spring
- 【Spring註解驅動開發】使用@Scope註解設定元件的作用域Spring元件
- spring框架半自動註解Spring框架
- 【寫框架】基於編譯時註解打造ActivityBus,一鍵傳值框架編譯