Spring AOP學習筆記05:AOP失效的罪因

  前面的文章中我們介紹了Spring AOP的簡單使用,並從源碼的角度學習了其底層的實現原理,有了這些基礎之後,本文來討論一下Spring AOP失效的問題,這個問題可能我們在平時工作中或多或少也會碰到。這個話題應該從同一個對象內的嵌套方法調用攔截失效說起。

1. 問題的現象

  假設我們有如下對象類定義(同一對象內方法嵌套調用的目標對象示例):

public class NestableInvocationDemo {
    public void method1(){
        method2();
        System.out.println("method1 executed!");
    }

    public void method2(){
        System.out.println("method2 executed!");
    }
}

  這個類定義中需要我們關注的是它的某個方法會調用同一對象上定義的其他方法。這通常是比較常見的,在NestableInvocationDemo類中,method1()方法調用了同一個對象的method2()方法。

  現在,我們要使用Spring AOP攔截該類定義的method1()和method2()方法,比如一個簡單的性能檢測,我們定義一個Aspect:

@Aspect
public class PerformanceTraceAspect {

    @Pointcut("execution(public void *.method1())")
    public void method1(){}

    @Pointcut("execution(public void *.method2())")
    public void method2(){}

    @Pointcut("method1() || method2()")
    public void compositePointcut(){};

    @Around("compositePointcut()")
    public Object performanceTrace(ProceedingJoinPoint joinpoint) throws Throwable{
        StopWatch watch = new StopWatch();
        try{
            watch.start();
            return joinpoint.proceed();
        }finally{
            watch.stop();
            System.out.println("PT in method[" + joinpoint.getSignature().getName() + "]>>>>" + watch.toString());
        }
    }
}

配置文件如下: 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop = "http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
     http://www.springframework.org/schema/aop
     http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
     
    <aop:aspectj-autoproxy/>
    <bean id = "nestableInvocationDemo" class = "xxx.xxx.NestableInvocationDemo"></bean>
    <bean class = "xxx.xxx.PerformanceTraceAspect"></bean>
</beans>

執行如下代碼:

public static void main(String[] args) { 

    ClassPathXmlApplicationContext factory = new ClassPathXmlApplicationContext("spring/demo/aop.xml");
    NestableInvocationDemo demo = factory.getBean("nestableInvocationDemo", NestableInvocationDemo.class);
    demo.method2();
    demo.method1();

}

輸出如下結果:

method2 executed!
PT in method[method2]>>>>StopWatch '': running time (millis) = 11; [] took 11 = 100%
method2 executed!
method1 executed!
PT in method[method1]>>>>StopWatch '': running time (millis) = 0; [] took 0 = 0%

   發現問題沒有?當我們從外部直接調用NestableInvocationDemo對象的method2()時,顯示攔截成功了,但是當調用method1()時,卻只有method1()方法的執行攔截成功,其內部的method2()方法執行卻沒有被攔截,因爲輸出日誌只有PT in method[method1]的信息。這說明部分AOP失效了,這是什麼原因呢,我們接着往下看。

 

2. 原因的分析

  這種結果的出現,歸根結底是由Spring AOP的實現機制造成的。我們知道,Spring AOP採用代理模式實現AOP,具體的橫切邏輯會被添加到動態生成的代理對象中,只要調用的是目標對象的代理對象上的方法,通常就可以保證目標對象上的方法可以被攔截。就像NestableInvocationDemo的method2()方法執行一樣,當我們調用代理對象上的method2()時,目標對象的method2()就會被成功攔截。

  不過,代理模式的實現機制在處理方法調用的時序方面,會給使用這種機制實現的AOP產品造成一個小小的“缺憾”。我們來看一下代理對象方法與目標對象方法的調用時序:

proxy.method2{
    記錄方法調用開始時間;
    target.method2;
    記錄方法調用結束時間;
    計算消耗的時間並記錄到日誌;
}

  在代理對象方法中,不管如何添加橫切邏輯,也不管添加多少橫切邏輯,有一點是確定的。那就是,終歸需要調用目標對象上的同一方法來執行最初所定義的方法邏輯。

  如果目標對象中原始方法調用依賴於其他對象,那沒問題,我們可以爲目標對象注入所依賴對象的代理,並且可以保證相應Joinpoint被攔截並織入橫切邏輯。而一旦目標對象中的原始方法調用直接調用自身方法的時候,也就是說,它依賴於自身所定義的其他方法的時候,問題就來了,看下面的圖會更清楚。

  在代理對象的method1方法執行經歷了層層攔截器之後,最終會將調用轉向目標對象上的method1,之後的調用流程全部是走在TargetObject之上,當method1調用method2時,它調用的是TargetObject上的method2,而不是ProxyObject上的method2。要知道,針對method2的橫切邏輯,只織入到了ProxyObject上的method2方法中,所以,在method1中所調用的method2沒有能夠被成功攔截。

 

3. 解決方案

  知道原因,我們纔可以“對症下藥”了。

  當目標對象依賴於其他對象時,我們可以通過爲目標註入依賴對象的代理對象,來解決相應的攔截問題。那麼,當目標對象依賴於自身時,我們也可以嘗試將目標對象的代理對象公開給它,只要讓目標對象調用自身代理對象上的相應方法,就可以解決內部調用的方法沒有被攔截的問題。

   Spring AOP提供了AopContext來公開當前目標對象的代理對象,我們只要在目標對象中使用AopContext.currentProxy()就可以取得當前目標對象所對應的代理對象。現在,我們重構目標對象,讓它直接調用它的代理對象的相應方法,如下面代碼所示:

public class NestableInvocationDemo {
    public void method1(){
        ((NestableInvocationDemo)AopContext.currentProxy()).method2();
        System.out.println("method1 executed!");
    }

    public void method2(){
        System.out.println("method2 executed!");
    }
}

  要使AopContext.currentProxy()生效,我們在生成目標對象的代理對象時,需要設置expose-proxy爲true,具體如下設置:

  在基於配置文件的配置中,可按如下方式配置:

<aop:aspectj-autoproxy expose-proxy = "true"/>

  在基於註解地配置中,可按如下方式配置:

@EnableAspectJAutoProxy(proxyTargteClass = true, exposeProxy = true)

  現在,我們可以得到想要的攔截結果:

method2 executed!
PT in method[method2]>>>>StopWatch '': running time (millis) = 12; [] took 12 = 100%
method2 executed!
PT in method[method2]>>>>StopWatch '': running time (millis) = 0; [] took 0 = 0%
method1 executed!
PT in method[method1]>>>>StopWatch '': running time (millis) = 0; [] took 0 = 0%

  這種方式是可以解決問題,但是不是很優雅,因爲我們的目標對象都直接綁定到了Spring AOP的具體API上了。所以,我們考慮能夠通過其他方式來解決這個問題,既然我們知道能夠通過AopContext.currentProxy()取得當前目標對象對應的代理對象,那完全可以在目標對象中聲明對其代理對象的依賴,通過IoC容器來幫助我們注入這個代理對象。

  注入方式可以有多種:

  • 可以在目標對象中聲明一個實例變量作爲其代理對象的引用,然後由構造方法注入或者setter方法注入將AopContext.currentProxy()取得的Object注入給這個聲明的實例變量;
  • 在目標對象中聲明一個getter方法,如getThis(),然後通過Spring的IoC容器的方法注入或者方法替換,將這個方法的邏輯替換爲return AopContext.currentProxy()。這樣,在調用自身方法的時候,直接通過getThis().method2()就可以了;
  • 聲明一個Wrapper類,並且讓目標對象依賴於這個類。在Wrapper類中直接聲明一個getProxy()或者類似的方法,將return AopContext.currentProxy()類似邏輯添加到這個方法中,目標對象只需要getWrapper().getProxy()就可以取得相應的代理對象。Wrapper類分離了目標對象與Spring API的直接耦合。至於讓這個Wrapper以Util類出現,還是在目標對象中直接構造,或者依賴注入到目標對象,都可以;
  • 爲類似的目標對象聲明統一的接口定義,然後通過BeanPostProcessor處理這些接口實現類,將實現類的某個取得當前對象的代理對象的方法邏輯覆蓋掉。這個與方法替換所使用的原理一樣,只不過可以藉助Spring的IoC容器進行批量處理而已。

  實際上,這種情況的出現僅僅是因爲Spring AOP採用的是代理機制實現。如果像AspectJ那樣,直接將橫切邏輯織入目標對象,那麼代理對象和目標對象實際上就合爲一體了,調用也不會出現這樣的問題。

 

4. 總結 

  本文揭示了Spring AOP實現機制導致的一個小小的陷阱,分析了問題產生的原因,並給出了一些解決方案。

  應該說,Spring AOP作爲一個輕量的AOP框架,在簡單與強大之間取得了很好的平衡。合理地使用Spring AOP,將幫助我們更快更好地完成各種工作,也希望大家在Spring AOP地使用之路上愉快地前行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章