代理機制
Spring AOP 使用 JDK 動態代理或 CGLIB 來為給定的目標物件建立代理。JDK 動態代理內建於 JDK 中,而 CGLIB 是一個常見的開放原始碼類別定義庫(重新封裝到 spring-core
中)。
如果要代理的目標物件至少實作一個介面,則使用 JDK 動態代理,並且代理目標類型實作的所有介面。如果目標物件未實作任何介面,則會建立 CGLIB 代理,它是目標類型的執行階段產生子類別。
如果您想強制使用 CGLIB 代理(例如,代理為目標物件定義的每個方法,而不僅僅是其介面實作的方法),您可以這樣做。但是,您應該考慮以下問題
-
final
類別無法代理,因為它們無法擴充。 -
final
方法無法建議,因為它們無法覆寫。 -
private
方法無法建議,因為它們無法覆寫。 -
不可見的方法(例如,來自不同套件的父類別中的套件私有方法)無法建議,因為它們實際上是私有的。
-
您的代理物件的建構子不會被呼叫兩次,因為 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">
<!-- other beans defined here... -->
</aop:config>
若要在使用 @AspectJ 自動代理支援時強制使用 CGLIB 代理,請將 <aop:aspectj-autoproxy>
元素的 proxy-target-class
屬性設定為 true
,如下所示
<aop:aspectj-autoproxy proxy-target-class="true"/>
多個 為了清楚起見,在 |
理解 AOP 代理
Spring AOP 是基於代理的。至關重要的是,在您編寫自己的切面或使用 Spring Framework 提供的任何基於 Spring AOP 的切面之前,您必須掌握最後一句話的語意。
首先考慮您有一個普通的、未代理的物件參考的場景,如下面的程式碼片段所示
-
Java
-
Kotlin
public class SimplePojo implements Pojo {
public void foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar();
}
public void bar() {
// some logic...
}
}
class SimplePojo : Pojo {
fun foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar()
}
fun bar() {
// some logic...
}
}
如果您在物件參考上調用方法,則該方法會直接在該物件參考上調用,如下圖和清單所示

-
Java
-
Kotlin
public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// this is a direct method call on the 'pojo' reference
pojo.foo();
}
}
fun main() {
val pojo = SimplePojo()
// this is a direct method call on the 'pojo' reference
pojo.foo()
}
當用戶端程式碼擁有的參考是代理時,情況會略有變化。考慮下圖和程式碼片段

-
Java
-
Kotlin
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();
// this is a method call on the proxy!
pojo.foo();
}
}
fun main() {
val factory = ProxyFactory(SimplePojo())
factory.addInterface(Pojo::class.java)
factory.addAdvice(RetryAdvice())
val pojo = factory.proxy as Pojo
// this is a method call on the proxy!
pojo.foo()
}
這裡要理解的關鍵是 Main
類別的 main(..)
方法中的用戶端程式碼具有對代理的參考。這表示對該物件參考的方法呼叫是對代理的呼叫。因此,代理可以委派給與該特定方法呼叫相關的所有攔截器(通知)。但是,一旦呼叫最終到達目標物件(在本例中為 SimplePojo
參考),它可能對自身進行的任何方法呼叫(例如 this.bar()
或 this.foo()
)都將針對 this
參考而不是代理進行調用。這具有重要的意義。這表示自我調用不會導致與方法調用關聯的通知有機會運行。換句話說,透過顯式或隱式 this
參考的自我調用將繞過通知。
為了解決這個問題,您有以下選項。
- 避免自我調用
-
最好的方法(這裡的「最好」一詞是寬鬆使用的)是重構您的程式碼,使自我調用不會發生。這確實需要您進行一些工作,但這是最好、侵入性最小的方法。
- 注入自我參考
-
另一種方法是使用自我注入,並透過自我參考而不是透過
this
調用代理上的方法。 - 使用
AopContext.currentProxy()
-
最後一種方法非常不鼓勵使用,我們猶豫要指出它,而是傾向於先前的選項。但是,作為最後的手段,您可以選擇將類別中的邏輯與 Spring AOP 綁定,如下例所示。
-
Java
-
Kotlin
public class SimplePojo implements Pojo {
public void foo() {
// This works, but it should be avoided if possible.
((Pojo) AopContext.currentProxy()).bar();
}
public void bar() {
// some logic...
}
}
class SimplePojo : Pojo {
fun foo() {
// This works, but it should be avoided if possible.
(AopContext.currentProxy() as Pojo).bar()
}
fun bar() {
// some logic...
}
}
使用 AopContext.currentProxy()
完全將您的程式碼耦合到 Spring AOP,並且它使類別本身意識到它正在 AOP 環境中使用,這減少了 AOP 的一些優點。它還要求將 ProxyFactory
配置為公開代理,如下例所示
-
Java
-
Kotlin
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
factory.setExposeProxy(true);
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
fun main() {
val factory = ProxyFactory(SimplePojo())
factory.addInterface(Pojo::class.java)
factory.addAdvice(RetryAdvice())
factory.isExposeProxy = true
val pojo = factory.proxy as Pojo
// this is a method call on the proxy!
pojo.foo()
}
AspectJ 編譯時織入和載入時織入沒有此自我調用問題,因為它們在位元組碼內而不是透過代理應用通知。 |