Spring 中的 Advice API

現在我們可以檢視 Spring AOP 如何處理通知 (advice)。

Advice 生命周期

每個通知都是一個 Spring Bean。一個通知實例可以在所有 advised 物件之間共享,或是每個 advised 物件獨有。這對應於每類別或每實例通知。

每類別通知最常使用。它適用於通用通知,例如交易 Advisor。這些不依賴代理物件的狀態,也不會新增狀態。它們僅作用於方法和引數。

每實例通知適用於引介 (introductions),以支援混入 (mixins)。在這種情況下,通知會將狀態新增至代理物件。

您可以在同一個 AOP 代理中使用共享和每實例通知的組合。

Spring 中的 Advice 類型

Spring 提供了幾種通知類型,並且可擴充以支援任意通知類型。本節描述基本概念和標準通知類型。

攔截環繞通知

Spring 中最基本的通知類型是攔截環繞通知。

Spring 符合 AOP Alliance 介面,用於使用方法攔截的環繞通知。實作 MethodInterceptor 且實作環繞通知的類別也應實作以下介面

public interface MethodInterceptor extends Interceptor {

	Object invoke(MethodInvocation invocation) throws Throwable;
}

MethodInvocation 引數傳遞給 invoke() 方法,公開了正在調用的方法、目標連接點、AOP 代理以及方法的引數。invoke() 方法應傳回調用的結果:連接點的傳回值。

以下範例顯示了一個簡單的 MethodInterceptor 實作

  • Java

  • Kotlin

public class DebugInterceptor implements MethodInterceptor {

	public Object invoke(MethodInvocation invocation) throws Throwable {
		System.out.println("Before: invocation=[" + invocation + "]");
		Object rval = invocation.proceed();
		System.out.println("Invocation returned");
		return rval;
	}
}
class DebugInterceptor : MethodInterceptor {

	override fun invoke(invocation: MethodInvocation): Any {
		println("Before: invocation=[$invocation]")
		val rval = invocation.proceed()
		println("Invocation returned")
		return rval
	}
}

請注意對 MethodInvocationproceed() 方法的呼叫。這會沿著攔截器鏈向下進行到連接點。大多數攔截器會調用此方法並傳回其傳回值。但是,MethodInterceptor,就像任何環繞通知一樣,可以傳回不同的值或拋出例外,而不是調用 proceed 方法。但是,除非有充分的理由,否則您不希望這樣做。

MethodInterceptor 實作提供了與其他符合 AOP Alliance 的 AOP 實作的互操作性。本節其餘部分討論的其他通知類型實作了常見的 AOP 概念,但以 Spring 特有的方式實作。雖然使用最特定的通知類型具有優勢,但如果您可能希望在另一個 AOP 框架中執行切面,請堅持使用 MethodInterceptor 環繞通知。請注意,切入點目前在框架之間不具互操作性,並且 AOP Alliance 目前未定義切入點介面。

前置通知

更簡單的通知類型是前置通知。這不需要 MethodInvocation 物件,因為它僅在進入方法之前調用。

前置通知的主要優點是不需要調用 proceed() 方法,因此,沒有意外未能沿著攔截器鏈向下進行的可能性。

以下列表顯示了 MethodBeforeAdvice 介面

public interface MethodBeforeAdvice extends BeforeAdvice {

	void before(Method m, Object[] args, Object target) throws Throwable;
}

(Spring 的 API 設計允許欄位前置通知,儘管通常的物件適用於欄位攔截,並且 Spring 永遠不太可能實作它。)

請注意,傳回類型為 void。前置通知可以在連接點執行之前插入自訂行為,但無法變更傳回值。如果前置通知拋出例外,它會停止攔截器鏈的進一步執行。例外會沿著攔截器鏈傳播回去。如果它是未檢查的或在調用方法的簽名上,則會直接傳遞給客户端。否則,它會被 AOP 代理包裝在未檢查的例外中。

以下範例顯示了 Spring 中的前置通知,它會計算所有方法調用

  • Java

  • Kotlin

public class CountingBeforeAdvice implements MethodBeforeAdvice {

	private int count;

	public void before(Method m, Object[] args, Object target) throws Throwable {
		++count;
	}

	public int getCount() {
		return count;
	}
}
class CountingBeforeAdvice : MethodBeforeAdvice {

	var count: Int = 0

	override fun before(m: Method, args: Array<Any>, target: Any?) {
		++count
	}
}
前置通知可以與任何切入點一起使用。

拋出異常通知

如果連接點拋出例外,則在連接點傳回後調用拋出異常通知。 Spring 提供類型化的拋出異常通知。請注意,這表示 org.springframework.aop.ThrowsAdvice 介面不包含任何方法。它是一個標籤介面,用於識別給定的物件實作了一個或多個類型化的拋出異常通知方法。這些方法應採用以下形式

afterThrowing([Method, args, target], subclassOfThrowable)

僅最後一個引數是必需的。方法簽名可以有一個或四個引數,取決於通知方法是否對方法和引數感興趣。接下來的兩個列表顯示了拋出異常通知的範例類別。

如果拋出 RemoteException (包括來自子類別的例外),則會調用以下通知

  • Java

  • Kotlin

public class RemoteThrowsAdvice implements ThrowsAdvice {

	public void afterThrowing(RemoteException ex) throws Throwable {
		// Do something with remote exception
	}
}
class RemoteThrowsAdvice : ThrowsAdvice {

	fun afterThrowing(ex: RemoteException) {
		// Do something with remote exception
	}
}

與前面的通知不同,下一個範例宣告了四個引數,因此它可以存取調用的方法、方法引數和目標物件。如果拋出 ServletException,則會調用以下通知

  • Java

  • Kotlin

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

	public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
		// Do something with all arguments
	}
}
class ServletThrowsAdviceWithArguments : ThrowsAdvice {

	fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
		// Do something with all arguments
	}
}

最後一個範例說明瞭如何在單個類別中使用這兩個方法來處理 RemoteExceptionServletException。任意數量的拋出異常通知方法可以組合在單個類別中。以下列表顯示了最後一個範例

  • Java

  • Kotlin

public static class CombinedThrowsAdvice implements ThrowsAdvice {

	public void afterThrowing(RemoteException ex) throws Throwable {
		// Do something with remote exception
	}

	public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
		// Do something with all arguments
	}
}
class CombinedThrowsAdvice : ThrowsAdvice {

	fun afterThrowing(ex: RemoteException) {
		// Do something with remote exception
	}

	fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
		// Do something with all arguments
	}
}
如果拋出異常通知方法本身拋出例外,它會覆寫原始例外 (也就是說,它會將拋出的例外變更為使用者)。覆寫例外通常是 RuntimeException,它與任何方法簽名相容。但是,如果拋出異常通知方法拋出已檢查的例外,則它必須符合目標方法的宣告例外,因此在某種程度上與特定的目標方法簽名耦合。不要拋出與目標方法的簽名不相容的未宣告的已檢查例外!
拋出異常通知可以與任何切入點一起使用。

後置傳回通知

Spring 中的後置傳回通知必須實作 org.springframework.aop.AfterReturningAdvice 介面,以下列表顯示了該介面

public interface AfterReturningAdvice extends Advice {

	void afterReturning(Object returnValue, Method m, Object[] args, Object target)
			throws Throwable;
}

後置傳回通知可以存取傳回值 (它無法修改)、調用的方法、方法的引數和目標。

以下後置傳回通知會計算所有未拋出例外的成功方法調用

  • Java

  • Kotlin

public class CountingAfterReturningAdvice implements AfterReturningAdvice {

	private int count;

	public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
			throws Throwable {
		++count;
	}

	public int getCount() {
		return count;
	}
}
class CountingAfterReturningAdvice : AfterReturningAdvice {

	var count: Int = 0
		private set

	override fun afterReturning(returnValue: Any?, m: Method, args: Array<Any>, target: Any?) {
		++count
	}
}

此通知不會變更執行路徑。如果它拋出例外,則會將其拋出到攔截器鏈中,而不是傳回值。

後置傳回通知可以與任何切入點一起使用。

引介通知

Spring 將引介通知視為一種特殊的攔截通知。

引介需要 IntroductionAdvisorIntroductionInterceptor,它們實作以下介面

public interface IntroductionInterceptor extends MethodInterceptor {

	boolean implementsInterface(Class intf);
}

從 AOP Alliance MethodInterceptor 介面繼承的 invoke() 方法必須實作引介。也就是說,如果調用的方法在引介的介面上,則引介攔截器負責處理方法呼叫 — 它無法調用 proceed()

引介通知不能與任何切入點一起使用,因為它僅在類別層級而不是方法層級應用。您只能將引介通知與 IntroductionAdvisor 一起使用,後者具有以下方法

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

	ClassFilter getClassFilter();

	void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

	Class<?>[] getInterfaces();
}

沒有 MethodMatcher,因此,沒有與引介通知關聯的 Pointcut。只有類別篩選是合乎邏輯的。

getInterfaces() 方法傳回此 Advisor 引介的介面。

validateInterfaces() 方法在內部用於查看已組態的 IntroductionInterceptor 是否可以實作引介的介面。

考慮 Spring 測試套件中的範例,假設我們想要將以下介面引介到一個或多個物件

  • Java

  • Kotlin

public interface Lockable {
	void lock();
	void unlock();
	boolean locked();
}
interface Lockable {
	fun lock()
	fun unlock()
	fun locked(): Boolean
}

這說明了混入。我們希望能夠將 advised 物件強制轉換為 Lockable,無論它們的類型如何,並調用 lock 和 unlock 方法。如果我們調用 lock() 方法,我們希望所有 setter 方法都拋出 LockedException。因此,我們可以新增一個切面,該切面提供使物件不可變的能力,而無需它們對此有任何了解:AOP 的一個很好的範例。

首先,我們需要一個執行繁重工作的 IntroductionInterceptor。在本例中,我們擴充了 org.springframework.aop.support.DelegatingIntroductionInterceptor 便利類別。我們可以直接實作 IntroductionInterceptor,但是對於大多數情況,使用 DelegatingIntroductionInterceptor 是最佳選擇。

DelegatingIntroductionInterceptor 旨在將引介委派給引介介面的實際實作,從而隱藏使用攔截來執行此操作。您可以使用建構子引數將委派設定為任何物件。預設委派 (當使用無引數建構子時) 是 this。因此,在下一個範例中,委派是 DelegatingIntroductionInterceptorLockMixin 子類別。給定一個委派 (預設情況下為自身),DelegatingIntroductionInterceptor 實例會尋找委派實作的所有介面 (IntroductionInterceptor 除外),並支援針對其中任何一個的引介。諸如 LockMixin 之類的子類別可以調用 suppressInterface(Class intf) 方法來抑制不應公開的介面。但是,無論 IntroductionInterceptor 準備支援多少介面,使用的 IntroductionAdvisor 都會控制實際公開哪些介面。引介的介面會隱藏目標對同一介面的任何實作。

因此,LockMixin 擴充了 DelegatingIntroductionInterceptor 並實作了 Lockable 本身。超類別會自動檢測到可以支援 Lockable 進行引介,因此我們無需指定它。我們可以透過這種方式引介任意數量的介面。

請注意 locked 實例變數的使用。這有效地將額外狀態新增到目標物件中保存的狀態。

以下範例顯示了範例 LockMixin 類別

  • Java

  • Kotlin

public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {

	private boolean locked;

	public void lock() {
		this.locked = true;
	}

	public void unlock() {
		this.locked = false;
	}

	public boolean locked() {
		return this.locked;
	}

	public Object invoke(MethodInvocation invocation) throws Throwable {
		if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
			throw new LockedException();
		}
		return super.invoke(invocation);
	}

}
class LockMixin : DelegatingIntroductionInterceptor(), Lockable {

	private var locked: Boolean = false

	fun lock() {
		this.locked = true
	}

	fun unlock() {
		this.locked = false
	}

	fun locked(): Boolean {
		return this.locked
	}

	override fun invoke(invocation: MethodInvocation): Any? {
		if (locked() && invocation.method.name.indexOf("set") == 0) {
			throw LockedException()
		}
		return super.invoke(invocation)
	}

}

通常,您不需要覆寫 invoke() 方法。DelegatingIntroductionInterceptor 實作 (如果方法是引介的,則調用 delegate 方法,否則繼續前往連接點) 通常就足夠了。在本例中,我們需要新增一個檢查:如果在鎖定模式下,則無法調用 setter 方法。

所需的引介僅需保留一個不同的 LockMixin 實例,並指定引介的介面 (在本例中,僅 Lockable)。更複雜的範例可能會取得對引介攔截器的參考 (它將被定義為原型)。在這種情況下,沒有與 LockMixin 相關的組態,因此我們使用 new 建立它。以下範例顯示了我們的 LockMixinAdvisor 類別

  • Java

  • Kotlin

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

	public LockMixinAdvisor() {
		super(new LockMixin(), Lockable.class);
	}
}
class LockMixinAdvisor : DefaultIntroductionAdvisor(LockMixin(), Lockable::class.java)

我們可以非常簡單地應用此 Advisor,因為它不需要任何組態。(但是,不可能在沒有 IntroductionAdvisor 的情況下使用 IntroductionInterceptor。) 與引介一樣,Advisor 必須是每實例的,因為它是有狀態的。對於每個 advised 物件,我們都需要 LockMixinAdvisor 的不同實例,以及 LockMixin。Advisor 包含 advised 物件狀態的一部分。

我們可以透過使用 Advised.addAdvisor() 方法或 (建議的方式) 在 XML 組態中以程式化方式應用此 Advisor,就像任何其他 Advisor 一樣。以下討論的所有代理建立選擇,包括「自動代理建立器」,都可以正確處理引介和有狀態混入。