如何通過自定義注解實現AOP切點定義


面向切面編程(Aspect Oriented Programming, AOP)是面向對象編程(Object Oriented Programming,OOP)的強大補充,通過橫切面注入的方式引入其他額外功能,比如日志記錄,事務處理等,用戶無需修改源代碼就可以"優雅"的實現額外功能的補充,對於Programmer來說,AOP是個非常強大的工具。

AOP中的切面處理邏輯會被應用到我們所定義的切點(Point Cut)上,切面邏輯定義可以使用 around, before,after等Aspect注解實現,切點可以使用Aspect注解中的參數指定或者通過xml配置文件聲明。在編寫代碼的過程中,切點控制往往不夠靈活,需要我們在xml或者Aspect注解參數中指定方法的path,當切點較多,需要顆粒度更加細致的切點控制時,通常我們需要添加大量的切點定義代碼,這樣比較麻煩。通常呢我們我們可以通過結合自定義注解來解決這個問題,實現更加靈活的切點控制。

自定義注解實現AOP切點定義的背后原理說起來其實很簡單,通過掃描項目所有的類,然后過濾出標注點的位置,將切面自定義邏輯應用到標注點上,就實現了我們的業務需求。但是,技術上如何去實現呢?本文就這一問題,結合Java Spring AOP框架給出解答。

目標

我們的自定義注解需要具備以下功能:

  1. 類中方法添加注解,則這個方法成為切點
  2. 類添加注解,則這個類中所有的方法成為切點
  3. 抽象類方法添加注解,則抽閑類中的這個方法成為切點
  4. 抽象類添加注解,則抽象類中的所有方法成為切點
  5. 接口添加注解,則接口中定義的所有方法成為切點
  6. 接口中方法添加注解,則接口中的這個方法成為切點

JDK提供的關鍵類和方法

Class

Java最基本的元素稱為"類",類中可以包涵方法和屬性的定義。Class對象提供了很多有用的方法,可以幫助我們切點位置定位,比如:

  1. public native Class<? super T> getSuperclass();獲取當前類的父類
  2. public Class<?>[] getInterfaces();獲取當前類實現的接口
  3. public Method[] getDeclaredMethods() throws SecurityException 獲取當前類中聲明的方法
  4. public < A extends Annotation > A getAnnotation(Class < A > annotationClass)獲取當前類指定標簽的對象,若為空,表明當前類沒有標簽annotationClass。

Method

我們還使用到Method類的一些方法:

  1. public String getName(); 獲取方法的名字
  2. public < T extends Annotation > T getAnnotation(Class annotationClass);取當前類指定標簽的對象,若為空,表明當前類沒有標簽annotationClass

ProceedingJoinPoint

ProceedingJoinPoint接口提供了很多實用的函數,便於用戶獲取應用切面點函數具體的信息。下面四個接口是我們用的比較多的:

  1. Object proceed() throws Throwable; 調用要攔截的方法
  2. Object proceed(Object[] var1) throws Throwable;調用要攔截的方法,可以自定義傳入參數
  3. Object[] getArgs();獲取攔截方法的傳入參數
  4. Signature getSignature();獲取攔截方法的方法名

實現

XML配置

配置xml文件,使能AOP和Spring Bean自動裝配

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
			http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
			http://www.springframework.org/schema/aop
			http://www.springframework.org/schema/aop/spring-aop-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 使能AOP-->
    <aop:aspectj-autoproxy/>
    <!-- 自動裝載bean使能-->
    <context:component-scan base-package="com.mj.spring.aop"/>
    <context:annotation-config/>

</beans>

定義自定義標簽

我們的自定義標簽可以作用於類,方法上,運行時工作。這兒需要說一下兩個標簽@Target和Retention,@Target用來設置標簽的作用范圍:

  1. @Target(ElementType. FIELD)表示標簽只能用來修飾字段、枚舉的常量
  2. @Target(ElementType.METHOD)表示標簽只能用來修飾方法
  3. @Target(ElementType.TYPE) 標簽可用來修飾接口、類、枚舉、注解
  4. ...

@Retention用來修飾注解的生存范圍

  1. @Retention(RetentionPolicy.SOURCE) 注解僅存在於源碼中,在class字節碼文件中不包含

  2. @Retention(RetentionPolicy.CLASS) 默認的保留策略,注解會在class字節碼文件中存在,但運行時無法獲得,

  3. @Retention(RetentionPolicy.RUNTIME) 注解會在class字節碼文件中存在,在運行時可以通過反射獲取到

     @Retention(RetentionPolicy.RUNTIME)
     public @interface AOPLog4jAnnotation {
     
     }
    

注意到我們沒有添加Target標簽,不指定標簽的作用范圍,那么標簽適用於所有范圍。

定義切面類

完整的切面類代碼如下所示,類中實現了切面邏輯的定義和切點判斷的邏輯代碼。

@Component
@Aspect
public class APIProxy{

    private final static Log LOGGER = LogFactory.getLog(APIProxy.class);

    //切面應用范圍是在com.mj.spring.aop包下面所有函數
    @Around("execution(* com.mj.spring.aop..*.*(..))")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        String signatureName = joinPoint.getSignature().getName();
        Class<? extends Object> invokeClass = joinPoint.getTarget().getClass();
        if (isTagged(invokeClass, signatureName)) {
            LOGGER.info(signatureName + " is tagged");
            joinPoint.proceed();
            return;
        }
        joinPoint.proceed();

    }

    //掃描父類是否被打上標簽,或者父類中的這個方法是否被打傷標簽
    private boolean isTagged(Class invokeClass, String signatureName) {
        if (isTaggedInInterfaceOf(invokeClass, signatureName)) {
            return true;
        }
        if (!invokeClass.equals(Object.class)) {
            return isTaggedInClassOf(invokeClass, signatureName) ? true :
                    isTagged(invokeClass.getSuperclass(), signatureName);
        }
        return false;
    }

    //掃描當前類的接口
    private boolean isTaggedInInterfaceOf(Class invokeClass, String signatureName) {
        Class[] interfaces = invokeClass.getInterfaces();
        for (Class cas : interfaces) {
            return isTaggedInClassOf(cas, signatureName) ? true :
                    isTaggedInInterfaceOf(cas, signatureName);
        }
        return false;
    }

    //方法名為signatureName的方法tagged有兩種情況:方法本身被taged或者方法所在的類被taged
    private boolean isTaggedInClassOf(Class cas, String signatureName) {
        return Lists.newArrayList(cas.getDeclaredMethods())
                .stream().anyMatch(method ->
                        isMethodWithName(method, signatureName) && isMethodTagged(method)
                                || isMethodWithName(method, signatureName) && isClassTagged(cas));
    }

    private boolean isClassTagged(Class invokeClass) {
        return invokeClass.getAnnotation(AOPLog4jAnnotation.class) != null;
    }

    private boolean isMethodTagged(Method method) {
        return method.getAnnotation(AOPLog4jAnnotation.class) != null;
    }

    private boolean isMethodWithName(Method method, String name) {
        return method.getName().equals(name);
    }
}

下面代碼實現了一個around切面advice定義,切面邏輯的應用范圍是com.mj.spring.aop包下的所有的方法,判斷當前執行方法是否被打上標簽,如果打上標簽,那么執行我們額外添加的業務邏輯代碼,這里為簡單起見在方法運行前打了一個log,然后執行方法,返回。否則直接調用方法,不做任何額外處理。

//切面應用范圍是在com.mj.spring.aop包下面所有函數
    @Around("execution(* com.mj.spring.aop..*.*(..))")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        String signatureName = joinPoint.getSignature().getName();
        Class<? extends Object> invokeClass = joinPoint.getTarget().getClass();
        if (isTagged(invokeClass, signatureName)) {
            LOGGER.info(signatureName + " is tagged");
            joinPoint.proceed();
            return;
        }
        joinPoint.proceed();

    }

方法被打上自定義標簽有以下幾種可能:

  1. 該方法方法體被打上標簽
  2. 該方法所在類被打上標簽
  3. 該方法所在的API接口函數對應被打上標簽
  4. 該方法所在的API接口被打上標簽
  5. 該方法所在的抽象類被打上標簽
  6. 該方法所在的抽象類函數定義被打上標簽

對於接口函數來說,接口之間可以多重嵌套,搜尋接口中指定函數的標簽,需要采用遞歸的方式向上尋找,對於父類繼承也同樣如此。下面的代碼實現涵蓋了上面6種能性的所有判讀。

    //掃描父類是否被打上標簽,或者父類中的這個方法是否被打傷標簽
    private boolean isTagged(Class invokeClass, String signatureName)    
    {
        if (isTaggedInInterfaceOf(invokeClass, signatureName)) {
            return true;
        }
        if (!invokeClass.equals(Object.class)) {
            return isTaggedInClassOf(invokeClass, signatureName) ? true :
                    isTagged(invokeClass.getSuperclass(), signatureName);
        }
        return false;
    }

函數開始:

  1. 判斷當前名為signatureName的方法是否在invokeClass類所實現的API接口中被Tag。(實現3和4的判斷)
  2. 判斷當前類是否為Object.class,若不是則執行第三步,否則執行第四步
  3. 判斷當前名為signatureName的方法是否在類invokeClass中被tag(實現1和2的判斷)
  4. 上面三項沒有為真,則調用當前類的父類繼續遞歸(實現5和6的判斷)

判斷當前名為signatureName的方法是否在invokeClass類所實現的API接口中被Tag的代碼如下所示,首先獲取當前類所有接口,分別對每個接口類進行方法檢查,若檢查成功,則返回true,否則繼續向上遞歸。

    //掃描當前類的接口
    private boolean isTaggedInInterfaceOf(Class invokeClass, String signatureName) {
        Class[] interfaces = invokeClass.getInterfaces();
        for (Class cas : interfaces) {
            return isTaggedInClassOf(cas, signatureName) ? true :
                    isTaggedInInterfaceOf(cas, signatureName);
        }
        return false;
    }

判斷一個名為signatureName的方法在類cas中是否被tag的代碼如下所示:

    private boolean isTaggedInClassOf(Class cas, String signatureName) {
        return Lists.newArrayList(cas.getDeclaredMethods())
                .stream().anyMatch(method ->
                        isMethodWithName(method, signatureName) && isMethodTagged(method)
                                || isMethodWithName(method, signatureName)&& isClassTagged(cas));

代碼邏輯實現很簡單,判斷方法被tag條件為:該方法是在該類中同時(該方法體是被打上標簽或者類被打上標簽)

Conclusion

本文和大家分享了如何通過自定義注解實現AOP切點定義,希望能夠對大家有所幫助。本文完整的源碼,單元測試位於:< https://github.com/jma19/spring/tree/master/spring-aop >, 歡迎大家下載,批評指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM