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
}
}
請注意對 MethodInvocation 的 proceed() 方法的呼叫。這會沿著攔截器鏈向下執行,直到連線點。大多數攔截器都會呼叫此方法並返回其返回值。但是,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
}
}
最後一個例子說明了如何在單個類中使用這兩個方法來處理 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。)與引介通常一樣,通知器必須是每個例項的,因為它是有狀態的。對於每個被通知物件,我們需要一個不同的 LockMixinAdvisor 例項,因此也需要一個不同的 LockMixin 例項。通知器構成了被通知物件狀態的一部分。
我們可以透過使用 Advised.addAdvisor() 方法以程式設計方式應用此通知器,或者(推薦的方式)像任何其他通知器一樣在 XML 配置中應用。下面討論的所有代理建立選擇,包括“自動代理建立器”,都正確處理引介和有狀態混入。