代理機制

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:config/> 區段在執行階段會摺疊成單一統一的自動代理建立器,它會套用任何 <aop:config/> 區段(通常來自不同的 XML Bean 定義檔案)指定的最強代理設定。這也適用於 <tx:annotation-driven/><aop:aspectj-autoproxy/> 元素。

為了清楚起見,在 <tx:annotation-driven/><aop:aspectj-autoproxy/><aop:config/> 元素上使用 proxy-target-class="true" 會強制它們全部使用 CGLIB 代理。

理解 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...
	}
}

如果您在物件參考上調用方法,則該方法會直接在該物件參考上調用,如下圖和清單所示

aop proxy plain pojo call
  • 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()
}

當用戶端程式碼擁有的參考是代理時,情況會略有變化。考慮下圖和程式碼片段

aop proxy call
  • 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 編譯時織入和載入時織入沒有此自我調用問題,因為它們在位元組碼內而不是透過代理應用通知。