寫給新手!Spring AOP代理機(jī)制,必須清楚,否則各種失效
1. 代理機(jī)制
Spring AOP 使用 JDK 動(dòng)態(tài)代理或 CGLIB 來為給定的目標(biāo)對(duì)象創(chuàng)建代理。JDK 動(dòng)態(tài)代理是內(nèi)置在 JDK 中的,而 CGLIB 是一個(gè)常見的開源類定義庫(已重新打包到 spring-core 中)。
如果要被代理的目標(biāo)對(duì)象實(shí)現(xiàn)了至少一個(gè)接口,則使用 JDK 動(dòng)態(tài)代理,并且目標(biāo)類型實(shí)現(xiàn)的所有接口都將被代理。如果目標(biāo)對(duì)象沒有實(shí)現(xiàn)任何接口,則創(chuàng)建一個(gè) CGLIB 代理,這是一個(gè)運(yùn)行時(shí)生成的目標(biāo)類型的子類。
如果你希望強(qiáng)制使用 CGLIB 代理(例如,為了代理目標(biāo)對(duì)象定義的所有方法,而不僅僅是其接口實(shí)現(xiàn)的方法),你可以這樣做。但是,你必須考慮以下問題:
- final修飾的類不能代理,因?yàn)樗鼈儾荒芾^承。
- final修飾的方法不能被增強(qiáng),因?yàn)樗鼈儾荒鼙恢貙憽?/li>
- 私有方法(private methods)無法被增強(qiáng),因?yàn)樗鼈儾荒鼙恢貙憽?/li>
- 不可見的方法(例如,來自不同包的父類中的包私有方法)無法被增強(qiáng),因?yàn)樗鼈儗?shí)際上是私有的。
- 你的代理對(duì)象的構(gòu)造函數(shù)不會(huì)被調(diào)用兩次,因?yàn)?CGLIB 代理實(shí)例是通過 Objenesis(它能繞過構(gòu)造函數(shù)) 創(chuàng)建的。然而,如果你的 JVM 不允許繞過構(gòu)造函數(shù),你可能會(huì)看到兩次調(diào)用以及來自 Spring AOP 支持的相應(yīng)調(diào)試日志。
- 在使用 CGLIB 代理時(shí),你可能會(huì)遇到 Java 模塊系統(tǒng)的限制。作為一個(gè)典型的例子,當(dāng)在模塊路徑上部署時(shí),你不能為 java.lang 包中的類創(chuàng)建 CGLIB 代理。這樣的情況需要一個(gè) JVM 啟動(dòng)標(biāo)志 --add-opens=java.base/java.lang=ALL-UNNAMED,但該標(biāo)志對(duì)模塊不可用。
要強(qiáng)制使用 CGLIB 代理,請(qǐng)將 <aop:config> 元素的 proxy-target-class 屬性值設(shè)置為 true,如下所示:
<aop:config proxy-target-class="true">
</aop:config>
如果是基于Spring Boot環(huán)境,則通過如下配置:
spring:
aop:
proxy-target-class: true
該屬性默認(rèn)值即為true。你還可以使用注解的方式,如下所示:
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {}
如果同時(shí)配置了,那么自定義注解優(yōu)先于配置文件。
要在使用 @AspectJ 自動(dòng)代理支持時(shí)強(qiáng)制使用 CGLIB 代理,可以將 <aop:aspectj-autoproxy> 元素的 proxy-target-class 屬性值設(shè)置為 true,如下所示:
<aop:aspectj-autoproxy proxy-target-class="true"/>
以上關(guān)于代理機(jī)制及相關(guān)配置的說明。
2. 理解AOP代理
Spring AOP 是基于代理的。在你編寫自己的切面或使用 Spring 框架提供的任何基于 Spring AOP 的切面之前,非常重要的一點(diǎn)是你要理解這句話的實(shí)際含義。
首先,請(qǐng)看下面的代碼片段,它顯示了一個(gè)普通的、未代理的對(duì)象引用:
public class SimplePojo implements Pojo {
public void foo() {
this.bar();
}
public void bar() {
}
}
如果你調(diào)用一個(gè)對(duì)象引用上的方法,該方法將直接在該對(duì)象引用上被調(diào)用,如下圖和列表所示:
圖片
public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
pojo.foo();
}
}
當(dāng)客戶端代碼的引用是代理時(shí),情況會(huì)略有不同。請(qǐng)看下圖和代碼片段:
圖片
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
// 通過創(chuàng)建的代理對(duì)象調(diào)用
pojo.foo();
}
}
這里的關(guān)鍵是要理解,main(..) 方法中的客戶端代碼持有一個(gè)代理的引用。這意味著對(duì)該對(duì)象引用的方法調(diào)用實(shí)際上是調(diào)用代理的方法。因此,代理可以委托給與該特定方法調(diào)用相關(guān)的所有攔截器(通知)。然而,一旦調(diào)用到達(dá)目標(biāo)對(duì)象(在這個(gè)例子中是 SimplePojo ),目標(biāo)對(duì)象上可能進(jìn)行的任何方法調(diào)用,如 this.bar() 或 this.foo(),都將針對(duì) this 引用進(jìn)行調(diào)用,而不是代理。這一點(diǎn)非常重要。這意味著自我調(diào)用不會(huì)導(dǎo)致與方法調(diào)用相關(guān)聯(lián)的通知有機(jī)會(huì)運(yùn)行。換句話說,通過顯式或隱式的 this 引用進(jìn)行的自我調(diào)用將繞過通知。
要解決這個(gè)問題,可以通過以下方式:
- 避免自我調(diào)用最好的方法(這里“最好”一詞用得比較寬松)是重構(gòu)你的代碼,以避免自我調(diào)用的發(fā)生。這確實(shí)需要你做一些工作,但這是最好且侵入性最小的方法。
- 注入自己另一種方法是使用自我注入(self-injection),并通過自我引用而不是 this 來調(diào)用代理上的方法。
- 使用AopContext.currentProxy()最后這種方法極不推薦。然而,作為最后的手段,你可以選擇將類內(nèi)的邏輯綁定到 Spring AOP,如下例所示:
public class SimplePojo implements Pojo {
public void foo() {
((Pojo) AopContext.currentProxy()).bar() ;
}
public void bar() {
}
}
使用 AopContext.currentProxy() 完全將你的代碼耦合到 Spring AOP,并使類本身意識(shí)到它正在一個(gè) AOP 上下文中使用,這減少了 AOP 的一些好處。同時(shí)你還需要進(jìn)行如下的配置才能使用如上方法:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
// 必須設(shè)置為true,你才能調(diào)用上面的方法
factory.setExposeProxy(true);
Pojo pojo = (Pojo) factory.getProxy() ;
pojo.foo();
}
}
如果你是Spring Boot環(huán)境,那么你可以通過下面的方式進(jìn)行設(shè)置:
@EnableAspectJAutoProxy(exposeProxy = true)
public class AppConfig {}
以上內(nèi)容,對(duì)于我們學(xué)習(xí)使用AOP新手來說必須要知道及掌握的內(nèi)容,這樣可以盡可能的減少錯(cuò)誤的發(fā)生。