Spring 中的通知 API

現在我們可以研究 Spring AOP 如何處理通知。

通知生命週期

每個通知都是一個 Spring Bean。一個通知例項可以跨所有被通知物件共享,也可以對於每個被通知物件都是唯一的。這對應於類級別通知(per-class advice)或例項級別通知(per-instance advice)。

類級別通知最常使用。它適用於通用通知,例如事務通知器(transaction advisors)。這些通知器不依賴於代理物件的狀態或新增新狀態。它們僅作用於方法和引數。

例項級別通知適用於引入(introductions),以支援 mixin。在這種情況下,通知會向代理物件新增狀態。

你可以在同一個 AOP 代理中使用共享通知和例項級別通知的組合。

Spring 中的通知型別

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

環繞攔截通知

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

Spring 遵循 AOP Alliance 的環繞通知介面,該介面使用方法攔截。因此,實現環繞通知的類應該實現 org.aopalliance.intercept 包中的以下 MethodInterceptor 介面

public interface MethodInterceptor extends Interceptor {

	Object invoke(MethodInvocation invocation) throws Throwable;
}

invoke() 方法的 MethodInvocation 引數暴露了正在呼叫的方法、目標連線點、AOP 代理以及方法的引數。invoke() 方法應該返回呼叫的結果:通常是連線點的返回值。

以下示例展示了一個簡單的 MethodInterceptor 實現

  • Java

  • Kotlin

public class DebugInterceptor implements MethodInterceptor {

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

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

注意對 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;
}

注意返回型別是 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();
}

引入通知沒有關聯的 MethodMatcherPointcut。只有類過濾是合理的。

getInterfaces() 方法返回此通知器引入的介面。

validateInterfaces() 方法在內部用於檢查配置的 IntroductionInterceptor 是否可以實現引入的介面。

考慮 Spring 測試套件中的一個示例,假設我們想向一個或多個物件引入以下介面

  • Java

  • Kotlin

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

這說明了一個 mixin。我們希望能夠將任何型別的被通知物件強制轉換為 Lockable,並呼叫 lock 和 unlock 方法。如果我們呼叫 lock() 方法,我們希望所有 setter 方法都丟擲 LockedException。因此,我們可以新增一個切面,使其能夠使物件不可變,而無需物件本身知道這一點:這是 AOP 的一個很好的示例。

首先,我們需要一個負責繁重工作的 IntroductionInterceptor。在此示例中,我們擴充套件了便捷類 org.springframework.aop.support.DelegatingIntroductionInterceptor。我們可以直接實現 IntroductionInterceptor,但在大多數情況下,使用 DelegatingIntroductionInterceptor 是最佳選擇。

DelegatingIntroductionInterceptor 的設計目的是將引入邏輯委託給引入介面的實際實現,同時隱藏使用攔截來實現這一目的。你可以使用建構函式引數將委託物件設定為任何物件。預設的委託物件(在使用無參建構函式時)是 this。因此,在下一個示例中,委託物件是 DelegatingIntroductionInterceptor 的子類 LockMixin。給定一個委託物件(預設情況下是其自身),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)

我們可以非常簡單地應用這個通知器,因為它不需要配置。(然而,沒有 IntroductionAdvisor 就不可能使用 IntroductionInterceptor。)與引入通知一樣,通知器必須是例項級別的(per-instance),因為它是 estatales。每個被通知物件都需要一個不同的 LockMixinAdvisor 例項,也因此需要不同的 LockMixin 例項。通知器構成了被通知物件狀態的一部分。

我們可以透過使用 Advised.addAdvisor() 方法以程式設計式方式應用此通知器,或者(推薦方式)像其他任何通知器一樣在 XML 配置中應用。下面討論的所有代理建立選項,包括“自動代理建立器”,都能正確處理引入通知和有狀態的 mixin。