Spring AOP 對(duì)象內(nèi)部方法間的嵌套調(diào)用方式
Spring AOP 對(duì)象內(nèi)部方法間的嵌套調(diào)用
前兩天面試的時(shí)候,面試官問(wèn)了一個(gè)問(wèn)題,大概意思就是一個(gè)類(lèi)有兩個(gè)成員方法 A 和 B,兩者都加了事務(wù)處理注解,定義了事務(wù)傳播級(jí)別為 REQUIRE_NEW,問(wèn) A 方法內(nèi)部直接調(diào)用 B 方法時(shí)能否觸發(fā)事務(wù)處理機(jī)制。
答案有點(diǎn)復(fù)雜,Spring 的事務(wù)處理其實(shí)是通過(guò)AOP實(shí)現(xiàn)的,而實(shí)現(xiàn)AOP的方法有好幾種,對(duì)于通過(guò) Jdk 和 cglib 實(shí)現(xiàn)的 aop 處理,上述問(wèn)題的答案為否,對(duì)于通過(guò)AspectJ實(shí)現(xiàn)的,上述問(wèn)題答案為是。
本文就結(jié)合具體例子來(lái)看一下
我們先定義一個(gè)接口
public interface AopActionInf {
void doSomething_01();
void doSomething_02();
}
以及此接口的一個(gè)實(shí)現(xiàn)類(lèi)
public class AopActionImpl implements AopActionInf{
public void doSomething_01() {
System.out.println("AopActionImpl.doSomething_01()");
//內(nèi)部調(diào)用方法 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("進(jìn)入環(huán)繞通知");
Object object = pjp.proceed();//執(zhí)行該方法
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>
運(yùn)行結(jié)果如下:

下圖是斷點(diǎn)分析在調(diào)用方法doSomething_02時(shí)的線程棧,很明顯在調(diào)用doSomething_02時(shí)并沒(méi)有對(duì)其進(jìn)行AOP處理。

默認(rèn)情況下,Spring AOP使用Jdk的動(dòng)態(tài)代理機(jī)制實(shí)現(xiàn),當(dāng)然也可以通過(guò)如下配置更改為cglib實(shí)現(xiàn),但是運(yùn)行結(jié)果相同,此處不再贅述。
<aop:aspectj-autoproxy proxy-target-class="true"/>
那有沒(méi)有辦法能夠觸發(fā)AOP處理呢?答案是有的,考慮到AOP是通過(guò)動(dòng)態(tài)生成目標(biāo)對(duì)象的代理對(duì)象而實(shí)現(xiàn)的,那么只要在調(diào)用方法時(shí)改為調(diào)用代理對(duì)象的目標(biāo)方法即可。
我們將調(diào)用 doSomething_02 的那行代碼改成如下,并修改相應(yīng)配置信息:
public void doSomething_01() {
System.out.println("AopActionImpl.doSomething_01()");
((AopActionInf) AopContext.currentProxy()).doSomething_02();
}
<aop:aspectj-autoproxy expose-proxy="true"/>
先來(lái)看一下運(yùn)行結(jié)果,

從運(yùn)行結(jié)果可以看出,嵌套調(diào)用方法已經(jīng)能夠?qū)崿F(xiàn)AOP處理了,同樣我們看一下線程調(diào)用棧信息,顯然 doSomething_02 方法被增強(qiáng)處理了(紅框中內(nèi)容)。

同一對(duì)象內(nèi)的嵌套方法調(diào)用AOP失效原因分析
舉一個(gè)同一對(duì)象內(nèi)的嵌套方法調(diào)用攔截失效的例子
首先定義一個(gè)目標(biāo)對(duì)象:
/**
* @description: 目標(biāo)對(duì)象與方法
* @create: 2020-12-20 17:10
*/
public class TargetClassDefinition {
public void method1(){
method2();
System.out.println("method1 執(zhí)行了……");
}
public void method2(){
System.out.println("method2 執(zhí)行了……");
}
}
在這個(gè)類(lèi)定義中,method1()方法會(huì)調(diào)用同一對(duì)象上的method2()方法。
現(xiàn)在,我們使用Spring AOP攔截該類(lèi)定義的method1()和method2()方法,比如一個(gè)簡(jiǎn)單的性能檢測(cè)邏輯,定義如下Aspect:
/**
* @description: 性能檢測(cè)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會(huì)攔截pointcutCombine()所指定的JoinPoint,即method1()或method2()的執(zhí)行。
接下來(lái)將AspectDefinition中定義的橫切邏輯織入TargetClassDefinition并運(yùn)行,其代碼如下:
/**
* @description: 啟動(dòng)方法
* @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();
}
}
執(zhí)行之后,得到如下結(jié)果:
method2 執(zhí)行了……
method1 執(zhí)行了……
PT in method [method1]>>>>>>StopWatch '': running time = 20855400 ns; [] took 20855400 ns = 100%
-------------------
method2 執(zhí)行了……
PT in method [method2]>>>>>>StopWatch '': running time = 71200 ns; [] took 71200 ns = 100%
不難發(fā)現(xiàn),從外部直接調(diào)用TargetClassDefinition的method2()方法的時(shí)候,因?yàn)樵摲椒ê灻ヅ銩spectDefinition中的Around Advice所對(duì)應(yīng)的Pointcut定義,所以Around Advice邏輯得以執(zhí)行,也就是說(shuō)AspectDefinition攔截method2()成功了。但是,當(dāng)調(diào)用method1()時(shí),只有method1()方法執(zhí)行攔截成功,而method1()方法內(nèi)部的method2()方法沒(méi)有執(zhí)行卻沒(méi)有被攔截。
原因分析
這種結(jié)果的出現(xiàn),歸根結(jié)底是Spring AOP的實(shí)現(xiàn)機(jī)制造成的。眾所周知Spring AOP使用代理模式實(shí)現(xiàn)AOP,具體的橫切邏輯會(huì)被添加到動(dòng)態(tài)生成的代理對(duì)象中,只要調(diào)用的是目標(biāo)對(duì)象的代理對(duì)象上的方法,通常就可以保證目標(biāo)對(duì)象上的方法執(zhí)行可以被攔截。就像TargetClassDefinition的method2()方法執(zhí)行一樣。
不過(guò),代理模式的實(shí)現(xiàn)機(jī)制在處理方法調(diào)用的時(shí)序方面,會(huì)給使用這種機(jī)制實(shí)現(xiàn)的AOP產(chǎn)品造成一個(gè)遺憾,一般的代理對(duì)象方法與目標(biāo)對(duì)象方法的調(diào)用時(shí)序如下所示:
proxy.method2(){
記錄方法調(diào)用開(kāi)始時(shí)間;
target.method2();
記錄方法調(diào)用結(jié)束時(shí)間;
計(jì)算消耗的時(shí)間并記錄到日志;
}
在代理對(duì)象方法中,無(wú)論如何添加橫切邏輯,不管添加多少橫切邏輯,最終還是需要調(diào)用目標(biāo)對(duì)象上的同一方法來(lái)執(zhí)行最初所定義的方法邏輯。
如果目標(biāo)對(duì)象中原始方法調(diào)用依賴于其他對(duì)象,我們可以為目標(biāo)對(duì)象注入所需依賴對(duì)象的代理,并且可以保證想用的JoinPoint被攔截并織入橫切邏輯。而一旦目標(biāo)對(duì)象中的原始方法直接調(diào)用自身方法的時(shí)候,也就是說(shuō)依賴于自身定義的其他方法時(shí),就會(huì)出現(xiàn)如下圖所示問(wèn)題:

在代理對(duì)象的method1()方法執(zhí)行經(jīng)歷了層層攔截器后,最終會(huì)將調(diào)用轉(zhuǎn)向目標(biāo)對(duì)象上的method1(),之后的調(diào)用流程全部都是在TargetClassDefinition中,當(dāng)method1()調(diào)用method2()時(shí),它調(diào)用的是TargetObject上的method2()而不是ProxyObject上的method2()。而針對(duì)method2()的橫切邏輯,只織入到了ProxyObject上的method2()方法中。所以,在method1()中調(diào)用的method2()沒(méi)有能夠被攔截成功。
解決方案
當(dāng)目標(biāo)對(duì)象依賴于其他對(duì)象時(shí),我們可以通過(guò)為目標(biāo)對(duì)象注入依賴對(duì)象的代理對(duì)象,來(lái)解決相應(yīng)的攔截問(wèn)題。
當(dāng)目標(biāo)對(duì)象依賴于自身時(shí),我們可以嘗試將目標(biāo)對(duì)象的代理對(duì)象公開(kāi)給它,只要讓目標(biāo)對(duì)象調(diào)用自身代理對(duì)象上的相應(yīng)方法,就可以解決內(nèi)部調(diào)用的方法沒(méi)有被攔截的問(wèn)題。
Spring AOP提供了AopContext來(lái)公開(kāi)當(dāng)前目標(biāo)對(duì)象的代理對(duì)象,我們只要在目標(biāo)對(duì)象中使用AopContext.currentProxy()就可以取得當(dāng)前目標(biāo)對(duì)象所對(duì)應(yīng)的代理對(duì)象。重構(gòu)目標(biāo)對(duì)象,如下所示:
import org.springframework.aop.framework.AopContext;
/**
* @description: 目標(biāo)對(duì)象與方法
* @create: 2020-12-20 17:10
*/
public class TargetClassDefinition {
public void method1(){
((TargetClassDefinition) AopContext.currentProxy()).method2();
// method2();
System.out.println("method1 執(zhí)行了……");
}
public void method2(){
System.out.println("method2 執(zhí)行了……");
}
}
要使AopContext.currentProxy()生效,需要在生成目標(biāo)對(duì)象的代理對(duì)象時(shí),將ProxyConfig或者它相應(yīng)的子類(lèi)的exposeProxy屬性設(shè)置為true,如下所示:
/**
* @description: 啟動(dòng)方法
* @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文件中的開(kāi)啟方式 -->
<aop:aspectj-autoproxy expose-proxy="true" />
再次執(zhí)行代碼,即可實(shí)現(xiàn)所需效果:
method2 執(zhí)行了……
PT in method [method2]>>>>>>StopWatch '': running time = 180400 ns; [] took 180400 ns = 100%
method1 執(zhí)行了……
PT in method [method1]>>>>>>StopWatch '': running time = 24027700 ns; [] took 24027700 ns = 100%
-------------------
method2 執(zhí)行了……
PT in method [method2]>>>>>>StopWatch '': running time = 64200 ns; [] took 64200 ns = 100%
后記
雖然通過(guò)將目標(biāo)對(duì)象的代理對(duì)象賦給目標(biāo)對(duì)象實(shí)現(xiàn)了我們的目的,但解決的方式不夠雅觀,我們的目標(biāo)對(duì)象都直接綁定到了Spring AOP的具體API上了。因此,在開(kāi)發(fā)中應(yīng)該盡量避免“自調(diào)用”的情況。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java正則表達(dá)式獲取指定HTML標(biāo)簽的指定屬性值且替換的方法
下面小編就為大家?guī)?lái)一篇java正則表達(dá)式獲取指定HTML標(biāo)簽的指定屬性值且替換的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-12-12
Java工廠模式優(yōu)雅地創(chuàng)建對(duì)象以及提高代碼復(fù)用率和靈活性
Java工廠模式是一種創(chuàng)建型設(shè)計(jì)模式,通過(guò)定義一個(gè)工廠類(lèi)來(lái)封裝對(duì)象的創(chuàng)建過(guò)程,將對(duì)象的創(chuàng)建和使用分離,提高代碼的可維護(hù)性和可擴(kuò)展性,同時(shí)可以實(shí)現(xiàn)更好的代碼復(fù)用和靈活性2023-05-05
java 如何給對(duì)象中的包裝類(lèi)設(shè)置默認(rèn)值
這篇文章主要介紹了java 如何給對(duì)象中的包裝類(lèi)設(shè)置默認(rèn)值,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
SpringMVC中的DispatcherServlet請(qǐng)求分析
這篇文章主要介紹了SpringMVC中的DispatcherServlet請(qǐng)求分析, DispatcherServlet作為一個(gè)Servlet,那么當(dāng)有請(qǐng)求到Tomcat等Servlet服務(wù)器時(shí),會(huì)調(diào)用其service方法,再調(diào)用到其父類(lèi)GenericServlet的service方法,需要的朋友可以參考下2024-01-01
使用jquery 的ajax 與 Java servlet的交互代碼實(shí)例
這篇文章主要介紹了使用jquery 的ajax 與 Java servlet的交互代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09
利用反射獲取Java類(lèi)中的靜態(tài)變量名及變量值的簡(jiǎn)單實(shí)例
下面小編就為大家?guī)?lái)一篇利用反射獲取Java類(lèi)中的靜態(tài)變量名及變量值的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-12-12
Spring定時(shí)任務(wù)中@PostConstruct被多次執(zhí)行異常的分析與解決
這篇文章主要給大家介紹了關(guān)于Spring定時(shí)任務(wù)中@PostConstruct被多次執(zhí)行異常的分析與解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-10-10
利用Java實(shí)現(xiàn)word導(dǎo)入導(dǎo)出富文本(含圖片)的詳細(xì)代碼
這篇文章主要為大家詳細(xì)介紹了利用Java實(shí)現(xiàn)word導(dǎo)入導(dǎo)出富文本(含圖片),文中的示例代碼講解詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,感興趣的小伙伴可以學(xué)習(xí)一下2024-02-02

