Spring AOP 對象內部方法間的嵌套調用方式

Spring AOP 對象內部方法間的嵌套調用

前兩天面試的時候,面試官問瞭一個問題,大概意思就是一個類有兩個成員方法 A 和 B,兩者都加瞭事務處理註解,定義瞭事務傳播級別為 REQUIRE_NEW,問 A 方法內部直接調用 B 方法時能否觸發事務處理機制。

答案有點復雜,Spring 的事務處理其實是通過AOP實現的,而實現AOP的方法有好幾種,對於通過 Jdk 和 cglib 實現的 aop 處理,上述問題的答案為否,對於通過AspectJ實現的,上述問題答案為是。

本文就結合具體例子來看一下

我們先定義一個接口

public interface AopActionInf {
    void doSomething_01();
    void doSomething_02();
}

以及此接口的一個實現類

public class AopActionImpl implements AopActionInf{
    public void doSomething_01() {
        System.out.println("AopActionImpl.doSomething_01()");
        //內部調用方法 doSomething_02
        this.doSomething_02();
    }
    public void doSomething_02() {
        System.out.println("AopActionImpl.doSomething_02()");
    }
}

增加AOP處理

public class ActionAspectXML {
    public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("進入環繞通知");
        Object object = pjp.proceed();//執行該方法
        System.out.println("退出方法");
        return object;
    }
}
<aop:aspectj-autoproxy/>
<bean id="actionImpl" class="com.maowei.learning.aop.AopActionImpl"/>
<bean id="actionAspectXML" class="com.maowei.learning.aop.ActionAspectXML"/>
<aop:config>
    <aop:aspect id = "aspectXML" ref="actionAspectXML">
        <aop:pointcut id="anyMethod" expression="execution(* com.maowei.learning.aop.AopActionImpl.*(..))"/>
        <aop:around method="aroundMethod" pointcut-ref="anyMethod"/>
    </aop:aspect>
</aop:config>

運行結果如下:

這裡寫圖片描述

下圖是斷點分析在調用方法doSomething_02時的線程棧,很明顯在調用doSomething_02時並沒有對其進行AOP處理。

默認情況下,Spring AOP使用Jdk的動態代理機制實現,當然也可以通過如下配置更改為cglib實現,但是運行結果相同,此處不再贅述。

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

那有沒有辦法能夠觸發AOP處理呢?答案是有的,考慮到AOP是通過動態生成目標對象的代理對象而實現的,那麼隻要在調用方法時改為調用代理對象的目標方法即可。

我們將調用 doSomething_02 的那行代碼改成如下,並修改相應配置信息:

public void doSomething_01() {
    System.out.println("AopActionImpl.doSomething_01()");
    ((AopActionInf) AopContext.currentProxy()).doSomething_02();
}
<aop:aspectj-autoproxy expose-proxy="true"/>

先來看一下運行結果,

這裡寫圖片描述

從運行結果可以看出,嵌套調用方法已經能夠實現AOP處理瞭,同樣我們看一下線程調用棧信息,顯然 doSomething_02 方法被增強處理瞭(紅框中內容)。

同一對象內的嵌套方法調用AOP失效原因分析

舉一個同一對象內的嵌套方法調用攔截失效的例子

首先定義一個目標對象:

/**
 * @description: 目標對象與方法
 * @create: 2020-12-20 17:10
 */
public class TargetClassDefinition {
    public void method1(){
        method2();
        System.out.println("method1 執行瞭……");
    }
    public void method2(){
        System.out.println("method2 執行瞭……");
    }
}

在這個類定義中,method1()方法會調用同一對象上的method2()方法。

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

/**
 * @description: 性能檢測Aspect定義
 * @create: 2020-12-20 17:13
 */
@Aspect
public class AspectDefinition {
    @Pointcut("execution(public void *.method1())")
    public void method1(){}
    @Pointcut("execution(public void *.method2())")
    public void method2(){}
    @Pointcut("method1() || method2()")
    public void pointcutCombine(){}
    @Around("pointcutCombine()")
    public Object aroundAdviceDef(ProceedingJoinPoint pjp) throws Throwable{
        StopWatch stopWatch = new StopWatch();
        try{
            stopWatch.start();
            return pjp.proceed();
        }finally {
            stopWatch.stop();
            System.out.println("PT in method [" + pjp.getSignature().getName() + "]>>>>>>"+stopWatch.toString());
        }
    }
}

由AspectDefinition定義可知,我們的Around Advice會攔截pointcutCombine()所指定的JoinPoint,即method1()或method2()的執行。

接下來將AspectDefinition中定義的橫切邏輯織入TargetClassDefinition並運行,其代碼如下:

/**
 * @description: 啟動方法
 * @create: 2020-12-20 17:23
 */
public class StartUpDefinition {
    public static void main(String[] args) {
        AspectJProxyFactory weaver = new AspectJProxyFactory(new TargetClassDefinition());
        weaver.setProxyTargetClass(true);
        weaver.addAspect(AspectDefinition.class);
        Object proxy = weaver.getProxy();
        ((TargetClassDefinition) proxy).method1();
        System.out.println("-------------------");
        ((TargetClassDefinition) proxy).method2();
    }
}

執行之後,得到如下結果:

method2 執行瞭……
method1 執行瞭……
PT in method [method1]>>>>>>StopWatch ”: running time = 20855400 ns; [] took 20855400 ns = 100%
——————-
method2 執行瞭……
PT in method [method2]>>>>>>StopWatch ”: running time = 71200 ns; [] took 71200 ns = 100%

不難發現,從外部直接調用TargetClassDefinition的method2()方法的時候,因為該方法簽名匹配AspectDefinition中的Around Advice所對應的Pointcut定義,所以Around Advice邏輯得以執行,也就是說AspectDefinition攔截method2()成功瞭。但是,當調用method1()時,隻有method1()方法執行攔截成功,而method1()方法內部的method2()方法沒有執行卻沒有被攔截。

原因分析

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

不過,代理模式的實現機制在處理方法調用的時序方面,會給使用這種機制實現的AOP產品造成一個遺憾,一般的代理對象方法與目標對象方法的調用時序如下所示:

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

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

如果目標對象中原始方法調用依賴於其他對象,我們可以為目標對象註入所需依賴對象的代理,並且可以保證想用的JoinPoint被攔截並織入橫切邏輯。而一旦目標對象中的原始方法直接調用自身方法的時候,也就是說依賴於自身定義的其他方法時,就會出現如下圖所示問題:

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

解決方案

當目標對象依賴於其他對象時,我們可以通過為目標對象註入依賴對象的代理對象,來解決相應的攔截問題。

當目標對象依賴於自身時,我們可以嘗試將目標對象的代理對象公開給它,隻要讓目標對象調用自身代理對象上的相應方法,就可以解決內部調用的方法沒有被攔截的問題。

Spring AOP提供瞭AopContext來公開當前目標對象的代理對象,我們隻要在目標對象中使用AopContext.currentProxy()就可以取得當前目標對象所對應的代理對象。重構目標對象,如下所示:

import org.springframework.aop.framework.AopContext;
/**
 * @description: 目標對象與方法
 * @create: 2020-12-20 17:10
 */
public class TargetClassDefinition {
    public void method1(){
        ((TargetClassDefinition) AopContext.currentProxy()).method2();
//        method2();
        System.out.println("method1 執行瞭……");
    }
    public void method2(){
        System.out.println("method2 執行瞭……");
    }
}

要使AopContext.currentProxy()生效,需要在生成目標對象的代理對象時,將ProxyConfig或者它相應的子類的exposeProxy屬性設置為true,如下所示:

/**
 * @description: 啟動方法
 * @create: 2020-12-20 17:23
 */
public class StartUpDefinition {
    public static void main(String[] args) {
        AspectJProxyFactory weaver = new AspectJProxyFactory(new TargetClassDefinition());
        weaver.setProxyTargetClass(true);
        weaver.setExposeProxy(true);
        weaver.addAspect(AspectDefinition.class);
        Object proxy = weaver.getProxy();
        ((TargetClassDefinition) proxy).method1();
        System.out.println("-------------------");
        ((TargetClassDefinition) proxy).method2();
    }
}
<!-- 在XML文件中的開啟方式 -->
<aop:aspectj-autoproxy expose-proxy="true" />

再次執行代碼,即可實現所需效果:

method2 執行瞭……
PT in method [method2]>>>>>>StopWatch ”: running time = 180400 ns; [] took 180400 ns = 100%
method1 執行瞭……
PT in method [method1]>>>>>>StopWatch ”: running time = 24027700 ns; [] took 24027700 ns = 100%
——————-
method2 執行瞭……
PT in method [method2]>>>>>>StopWatch ”: running time = 64200 ns; [] took 64200 ns = 100%

後記

雖然通過將目標對象的代理對象賦給目標對象實現瞭我們的目的,但解決的方式不夠雅觀,我們的目標對象都直接綁定到瞭Spring AOP的具體API上瞭。因此,在開發中應該盡量避免“自調用”的情況。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: