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