Spring 中的 Advice API

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

通知生命週期

每個通知都是一個 Spring bean。一個通知例項可以在所有被通知物件之間共享,也可以是每個被通知物件獨有的。這對應於每個類或每個例項的通知。

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

每個例項的通知適用於引入,以支援 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 實現的互操作性。本節其餘部分討論的其他通知型別以 Spring 特有的方式實現了常見的 AOP 概念。儘管使用最具體的通知型別有優勢,但如果您可能希望在另一個 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(包括 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 將引介通知視為一種特殊型別的攔截通知。

引介需要一個 IntroductionAdvisor 和一個實現以下介面的 IntroductionInterceptor

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() 方法返回此通知器引入的介面。

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。因此,在下一個示例中,委託是 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)

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

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

© . This site is unofficial and not affiliated with VMware.