同一對象內的嵌套方法調用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上了。因此,在開發中應該盡量避免“自調用”的情況。
摘自 《Spring 揭秘》 王福強 人民郵電出版社