Spring Boot學習分享(九)——AOP攔截方法的參數

    在進行Web MVC開發的時候,在編寫controller時總是要對請求的參數進行值的判斷,最常見的情況還是判斷其是否爲空。雖然有一些可以用的註解可以實現判斷,像是Lombok的@NotNull,SpringMVC的@RequestParam等,但總覺的不是很好用,因此萌生了自己自定義註解實現的念頭。
    想到使用AOP實現起來會十分方便,因此花了點時間研究了一下。


第一步 實現對參數的攔截

1. 註解的定義
/**
 * 檢驗參數是否爲空
 * 在方法上的註解只是校驗value裏面的參數是否爲空
 * 在參數上的註解需要指定參數類型,才能對參數的成員進行校驗
 */
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckNull {
    Class<?> type() default Object.class;

    String value() default "";
}

2. AOP攔截註解

    由於AOP的@Pointcut提供了很好用的表達式,可以參考一下官方文檔

    這裏貼一下自己所用到的expression,注意如何AOP類和註解在同個包下面,可以不寫包名,不是的話則必須指明包名。

    /**
     * 攔截註解
     */
    @Pointcut("@annotation(CheckNull) || execution(* *(@CheckNull (*),..))")
    public void check() {
    }
    

第二步 得到參數並處理

    通過上面定義的切口,現在已經能夠得到參數了,接下來只要拿到參數就可以了。這裏使用的是AOP的aroundresolveMethod等函數是我自定義的處理函數,文章最後會貼完整的代碼。
    這個地方需要注意的是ProceedingJoinPoint中方法的註解和參數的註解獲取有點坑,具體的可以看代碼瞭解一下。

    @Around(value = "check()")
    public Object around(ProceedingJoinPoint joinPoinresolveMethodt) throws Throwable {

        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //得到真實的方法對象
        Method realMethod = joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(),
                method.getParameterTypes());
        //拿到方法的註解
        Annotation[] annotations = realMethod.getDeclaredAnnotations();
        //得到參數列表
        Object[] args = joinPoint.getArgs();
        //得到參數名列表
        String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
        //先校驗方法上的註解
        String r1 = checkMethod(annotations, args, parameterNames);
        if (null == r1) {
            //再校驗參數上的註解
            r1 = resolveParam(args, realMethod.getParameterAnnotations());
            if (null == r1) {
            	// 沒有錯誤,繼續執行應當執行的函數
                return joinPoint.proceed();
            }
        }
        //適用於返回值爲json的controller
        return ResultUtil.error(ExceptionEnum.PARAMETER_NULL.getCode(), String.format(ExceptionEnum.PARAMETER_NULL.getMsg(), r1));
    }

完整代碼

    該代碼只是實現了對函數的參數的判空處理,如果要判斷的參數爲實體類中的某個屬性,則必須在方法的參數上使用註解,並標註實體類的類型。

@Aspect
@Log4j2
public class CheckNullAop {
    /**
     * 攔截方法上已經參數上的註解
     */
    @Pointcut("@annotation(CheckNull) || execution(* *(@CheckNull (*),..))")
    public void check() {
    }

    /**
     * 方法參數校驗
     *
     * @param annotations    方法上的註解數組
     * @param args           方法的參數值列表
     * @param parameterNames 方法的參數名列表
     */
    private String checkMethod(Annotation[] annotations, Object[] args, String[] parameterNames) {

        //校驗方法上的註解
        //考慮到有多個相同的註解
        for (Annotation annotation : annotations) {
            if (annotation instanceof CheckNull) {
                //先解析方法上的參數
                String result = resolveMethod((CheckNull) annotation, args, Arrays.asList(parameterNames));
                if (null != result)
                    return result;
            }
        }
        return null;
    }

    /**
     * 解析方法上的註解
     *
     * @param checkNull  註解
     * @param args       方法的參數值列表
     * @param parameters 方法的參數名列表
     */
    private String resolveMethod(CheckNull checkNull, Object[] args, List<String> parameters) {

        assert args.length == parameters.size();

        String[] list = checkNull.value().split(",");

        //直接遍歷value
        for (String s : list) {
            int index = parameters.indexOf(s);
            if (-1 != index && null == args[index]) {
                return s;
            }
        }

        return null;
    }

    /**
     * 解析參數上的註解
     *
     * @param args       方法的參數值列表
     * @param parameters 參數的註解列表,爲二維數組
     */
    private String resolveParam(Object[] args, Annotation[][] parameters) {

        for (int i = 0; i < parameters.length; ++i) {
            for (Annotation annotation : parameters[i]) {
                if (annotation instanceof CheckNull) {
                    CheckNull c = (CheckNull) annotation;
                    if (c.type() != Object.class) {
                        for (String s : c.value().split(",")) {
                            try {
                                //拿到不能訪問的屬性
                                //無法拿到父類屬性
                                Field field = args[i].getClass().getDeclaredField(s);
                                if (!field.isAccessible()) field.setAccessible(true);
                                if (null == field.get(args[i])) {
                                    return s;
                                }
                            } catch (NoSuchFieldException | IllegalAccessException e) {
                                log.warn(e.getMessage());
                            }
                        }
                    }
                    break;
                }
            }
        }

        return null;
    }

    @Around(value = "check()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //得到真實的方法對象
        Method realMethod = joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(),
                method.getParameterTypes());
        //拿到方法的註解
        Annotation[] annotations = realMethod.getDeclaredAnnotations();
        //得到參數列表
        Object[] args = joinPoint.getArgs();
        //得到參數名列表
        String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
        //先校驗方法上的註解
        String r1 = checkMethod(annotations, args, parameterNames);
        if (null == r1) {
            //再校驗參數上的註解
            r1 = resolveParam(args, realMethod.getParameterAnnotations());
            if (null == r1) {
                // 沒有錯誤,繼續進行
                return joinPoint.proceed();
            }
        }
        //適用於返回值爲json
        return ResultUtil.error(ExceptionEnum.PARAMETER_NULL.getCode(), String.format(ExceptionEnum.PARAMETER_NULL.getMsg(), r1));
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章