1. 程式人生 > >使用AOP實現引數驗證

使用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());
    }

這個做法在我看來,是不夠完美的,我想是否可以運用多型或者繼承,來實現驗證註解與驗證處理介面的聯絡?而非這樣進行手工註冊。

這裡是一個需要思考的地方。