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
}
}
注意對 MethodInvocation
的 proceed()
方法的呼叫。這會將執行流沿著攔截器鏈向下推進到連線點。大多數攔截器都會呼叫此方法並返回其返回值。然而,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
}
}
最後一個示例展示瞭如何在一個類中組合使用這兩個方法來處理 RemoteException
和 ServletException
。可以在一個類中組合任意數量的丟擲通知方法。以下列表展示了最後一個示例
-
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
。因此,在下一個示例中,委託物件是 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。