使用AOP實現引數驗證
今天,我們使用AOP來開發一個引數驗證。
首先,我們理理這個引數驗證的原理以及流程。
第一步,我們會定義一個註解,這個註解可以被用來修飾某一方法的引數,如下:
public Object login(@RequestBody @CustomValid LoginDto loginDto, HttpSession session){
return loginService.login(loginDto,session);
}
這裡的@CustomValid就是我們的自定義註解。
它的程式碼如下:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface CustomValid { }
第二步,我們會在切入點的環繞通知中獲取所有的方法引數:
//獲取所有的方法引數
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature)signature;
Method targetMethod = methodSignature.getMethod();
Parameter[] parameters=targetMethod.getParameters();
通過這一句程式碼來獲取需要驗證的方法引數型別:
List<Class> list=validationProcessor.getValidateClass(parameters);
其內部實現:
public List<Class> getValidateClass(Parameter[] parameters){ List<Class> list=new ArrayList<Class>(); for (Parameter parameter : parameters) { Annotation[] annotations=parameter.getAnnotations(); for (Annotation annotation : annotations) { if(annotation instanceof CustomValid){ list.add(parameter.getType()); } } } return list; }
原理就是通過反射獲取引數陣列中各個引數上的註解,並判斷是否有@CustomValid這個註解,如果有就加入列表,最終返回。
第三步,獲取到需要驗證的引數型別之後,我們進行雙重迴圈,找出需要進行引數驗證的引數物件:
for (Object arg : joinPoint.getArgs()) {
for (Class aClass : list) {
//將引數與需要驗證的引數型別進行匹配
if(arg.getClass().getName().equals(aClass.getName())){
try {
//如果驗證失敗
ValidResult validResult=validationProcessor.valid(arg.getClass().getDeclaredFields(),arg);
if(!validResult.isValid()){
return new Result<String>(ResultState.ERROR,validResult.getValidFailMessage());
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
這裡validationProcessor有一個valid方法,該方法通過引數物件的所有成員變數,並取出每個成員變數上的註解,進一步進行驗證,程式碼如下:
public ValidResult valid(Field[] fields, Object target) throws IllegalAccessException {
for (Field field : fields) {
//獲取每個成員的註解
Annotation[] annotations=field.getDeclaredAnnotations();
// 判斷註解是否在validMapping裡面
for (Annotation annotation : annotations) {
ValidInterface validInterface=validMapping.get(annotation.annotationType());
//如果validMapping擁有相關實現
if(validInterface!=null){
field.setAccessible(true);
//如果這個成員變數註解驗證失敗
if(! validInterface.valid(field.get(target))){
return new ValidResult(false,validInterface.getErrorMessage(field.getName()));
}
}
}
}
return new ValidResult(true);
}
這裡面有一個validMapping物件,該map維護驗證註解與內部驗證類的對映關係:
private HashMap<Class<? extends Annotation>,ValidInterface> validMapping=new HashMap<>();
{
//註冊驗證註解實現
validMapping.put(CustomNotNull.class, new CustomNotNull.Validation());
validMapping.put(CustomEmail.class,new CustomEmail.validation());
}
其中,每一個驗證註解都會實現一個ValidInterface介面,程式碼如下:
public interface ValidInterface {
boolean valid(Object object);
String getErrorMessage(String fieldName);
}
當驗證器進行驗證的話,valid方法會被呼叫,如果驗證通過,返回true 反之false。
getErrorMessage 則是返回驗證失敗後的提示資訊。
這是ValidResult的程式碼:
public class ValidResult {
private boolean isValid;
private String validFailMessage;
public ValidResult(boolean isValid,String validFailMessage){
this.isValid=isValid;
this.validFailMessage=validFailMessage;
}
public ValidResult(boolean isValid){
this.isValid=isValid;
}
}
其主要職責就是存放驗證結果。
這樣,一個完整的引數驗證流程就完成了,讓我們來總結一下:
1.迴圈判斷引數是否含有@CustomValid註解,若有則進行下一步,否則退出。
2.取出含有@CustomValid註解的引數物件,取出其所有的成員變數。
3.迴圈獲得每個成員變數上的註解,若在驗證處理器內部擁有註冊,則呼叫這個驗證註解提供的驗證介面。
4.返回驗證結果,讓切入點的環繞通知決定下一步結果。
不完美的地方:
在這裡,我們通過手工的方式進行了驗證介面的註冊:
private HashMap<Class<? extends Annotation>,ValidInterface> validMapping=new HashMap<>();
{
//註冊驗證註解實現
validMapping.put(CustomNotNull.class, new CustomNotNull.Validation());
validMapping.put(CustomEmail.class,new CustomEmail.validation());
}
這個做法在我看來,是不夠完美的,我想是否可以運用多型或者繼承,來實現驗證註解與驗證處理介面的聯絡?而非這樣進行手工註冊。
這裡是一個需要思考的地方。