基於 Schema 的 AOP 支援
如果您偏好使用基於 XML 的格式,Spring 也提供使用 aop
命名空間標籤來定義 Aspect 的支援。支援與使用 @AspectJ 風格時完全相同的 Pointcut 運算式和 Advice 種類。因此,在本節中,我們將重點放在該語法上,並請讀者參考前一節(@AspectJ 支援)中的討論,以了解如何編寫 Pointcut 運算式和 Advice 參數的繫結。
若要使用本節中描述的 aop 命名空間標籤,您需要匯入 spring-aop
Schema,如基於 XML Schema 的配置中所述。請參閱AOP Schema,了解如何在 aop
命名空間中匯入標籤。
在您的 Spring 配置中,所有 Aspect 和 Advisor 元素都必須放置在 <aop:config>
元素內(在應用程式 Context 配置中,您可以有多個 <aop:config>
元素)。<aop:config>
元素可以包含 Pointcut、Advisor 和 Aspect 元素(請注意,這些元素必須按該順序宣告)。
<aop:config> 風格的配置大量使用 Spring 的自動代理機制。如果您已經透過使用 BeanNameAutoProxyCreator 或類似機制來使用顯式自動代理,則可能會導致問題(例如 Advice 未被織入)。建議的使用模式是僅使用 <aop:config> 風格或僅使用 AutoProxyCreator 風格,切勿將它們混合使用。 |
宣告 Aspect
當您使用 Schema 支援時,Aspect 是一個常規 Java 物件,定義為 Spring 應用程式 Context 中的 Bean。狀態和行為捕獲在物件的欄位和方法中,而 Pointcut 和 Advice 資訊捕獲在 XML 中。
您可以使用 <aop:aspect>
元素宣告 Aspect,並使用 ref
屬性參考後端 Bean,如下例所示
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
支援 Aspect 的 Bean(在本例中為 aBean
)當然可以像任何其他 Spring Bean 一樣進行配置和依賴注入。
宣告 Pointcut
您可以在 <aop:config>
元素內宣告具名 Pointcut,讓 Pointcut 定義在多個 Aspect 和 Advisor 之間共用。
表示服務層中任何業務服務執行的 Pointcut 可以定義如下
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))" />
</aop:config>
請注意,Pointcut 運算式本身使用與@AspectJ 支援中描述的相同的 AspectJ Pointcut 運算式語言。如果您使用基於 Schema 的宣告樣式,您也可以在 Pointcut 運算式中參考在 @Aspect
類型中定義的具名 Pointcut。因此,定義上述 Pointcut 的另一種方法如下
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.CommonPointcuts.businessService()" /> (1)
</aop:config>
1 | 參考共用具名 Pointcut 定義中定義的 businessService 具名 Pointcut。 |
在 Aspect 內部宣告 Pointcut 與宣告頂層 Pointcut 非常相似,如下例所示
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
與 @AspectJ Aspect 非常相似,使用基於 Schema 的定義樣式宣告的 Pointcut 可以收集連接點 Context。例如,以下 Pointcut 收集 this
物件作為連接點 Context,並將其傳遞給 Advice
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
必須宣告 Advice 以接收收集的連接點 Context,方法是包含具有相符名稱的參數,如下所示
-
Java
-
Kotlin
public void monitor(Object service) {
// ...
}
fun monitor(service: Any) {
// ...
}
組合 Pointcut 子運算式時,&&
在 XML 文件中顯得笨拙,因此您可以使用 and
、or
和 not
關鍵字來代替 &&
、||
和 !
。例如,先前的 Pointcut 可以更好地寫成如下
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..)) and this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
請注意,以這種方式定義的 Pointcut 是透過其 XML id
參考的,並且不能用作具名 Pointcut 來形成複合 Pointcut。因此,基於 Schema 的定義樣式中的具名 Pointcut 支援比 @AspectJ 風格提供的更有限。
宣告 Advice
基於 Schema 的 AOP 支援使用與 @AspectJ 風格相同的五種 Advice,它們具有完全相同的語意。
前置通知
前置通知在符合條件的方法執行之前執行。它在 <aop:aspect>
內部使用 <aop:before>
元素宣告,如下例所示
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
在上面的範例中,dataAccessOperation
是在頂層 (<aop:config>
) 層級定義的具名 Pointcut 的 id
(請參閱宣告 Pointcut)。
正如我們在 @AspectJ 風格的討論中指出的那樣,使用具名 Pointcut 可以顯著提高程式碼的可讀性。請參閱共用具名 Pointcut 定義以取得詳細資訊。 |
若要內聯定義 Pointcut,請將 pointcut-ref
屬性替換為 pointcut
屬性,如下所示
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
method
屬性識別提供 Advice 主體的方法 (doAccessCheck
)。必須為包含 Advice 的 Aspect 元素參考的 Bean 定義此方法。在執行資料存取操作之前(Pointcut 運算式比對的方法執行連接點),將調用 Aspect Bean 上的 doAccessCheck
方法。
後置返回通知
後置返回通知在符合條件的方法執行正常完成時執行。它在 <aop:aspect>
內部以與前置通知相同的方式宣告。以下範例說明如何宣告它
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
與 @AspectJ 風格一樣,您可以在 Advice 主體中取得傳回值。若要執行此操作,請使用 returning
屬性來指定應將傳回值傳遞給的參數名稱,如下例所示
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
doAccessCheck
方法必須宣告一個名為 retVal
的參數。此參數的類型以與 @AfterReturning
描述的方式相同的約束比對。例如,您可以將方法簽章宣告如下
-
Java
-
Kotlin
public void doAccessCheck(Object retVal) {...
fun doAccessCheck(retVal: Any) {...
後置異常通知
後置異常通知在符合條件的方法執行因擲回例外而結束時執行。它在 <aop:aspect>
內部使用 after-throwing
元素宣告,如下例所示
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doRecoveryActions"/>
...
</aop:aspect>
與 @AspectJ 風格一樣,您可以在 Advice 主體中取得擲回的例外。若要執行此操作,請使用 throwing
屬性來指定應將例外傳遞給的參數名稱,如下例所示
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
doRecoveryActions
方法必須宣告一個名為 dataAccessEx
的參數。此參數的類型以與 @AfterThrowing
描述的方式相同的約束比對。例如,方法簽章可以宣告如下
-
Java
-
Kotlin
public void doRecoveryActions(DataAccessException dataAccessEx) {...
fun doRecoveryActions(dataAccessEx: DataAccessException) {...
後置(最終)通知
後置(最終)通知無論符合條件的方法執行如何結束都會執行。您可以使用 after
元素宣告它,如下例所示
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doReleaseLock"/>
...
</aop:aspect>
環繞通知
最後一種 Advice 是環繞通知。環繞通知「環繞」符合條件的方法執行而執行。它有機會在方法執行前後執行工作,並確定方法何時、如何甚至是否實際執行。如果您需要在執行緒安全的方式中在方法執行前後共用狀態,則通常使用環繞通知,例如,啟動和停止計時器。
始終使用滿足您需求的最弱形式的 Advice。 例如,如果前置通知足以滿足您的需求,請勿使用環繞通知。 |
您可以使用 aop:around
元素宣告環繞通知。Advice 方法應宣告 Object
作為其傳回類型,並且該方法的第一個參數必須是 ProceedingJoinPoint
類型。在 Advice 方法的主體中,您必須調用 ProceedingJoinPoint
上的 proceed()
,才能執行基礎方法。調用不帶引數的 proceed()
將導致在調用基礎方法時將調用者的原始引數提供給基礎方法。對於進階用例,proceed()
方法有一個重載變體,它接受引數陣列 (Object[]
)。陣列中的值將在調用基礎方法時用作基礎方法的引數。請參閱環繞通知,以取得有關使用 Object[]
調用 proceed
的注意事項。
以下範例說明如何在 XML 中宣告環繞通知
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut="execution(* com.xyz.service.*.*(..))"
method="doBasicProfiling"/>
...
</aop:aspect>
doBasicProfiling
Advice 的實作可以與 @AspectJ 範例中的實作完全相同(當然,不包括註解),如下例所示
-
Java
-
Kotlin
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
fun doBasicProfiling(pjp: ProceedingJoinPoint): Any? {
// start stopwatch
val retVal = pjp.proceed()
// stop stopwatch
return pjp.proceed()
}
Advice 參數
基於 Schema 的宣告樣式支援完全類型化的 Advice,方式與 @AspectJ 支援描述的方式相同 — 透過將 Pointcut 參數按名稱與 Advice 方法參數比對。請參閱Advice 參數以取得詳細資訊。如果您希望明確指定 Advice 方法的引數名稱(不依賴先前描述的偵測策略),您可以使用 Advice 元素的 arg-names
屬性來執行此操作,其處理方式與 Advice 註解中的 argNames
屬性相同(如確定引數名稱中所述)。以下範例說明如何在 XML 中指定引數名稱
<aop:before
pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)" (1)
method="audit"
arg-names="auditable" />
1 | 參考組合 Pointcut 運算式中定義的 publicMethod 具名 Pointcut。 |
arg-names
屬性接受逗號分隔的參數名稱列表。
以下稍微複雜的基於 XSD 的方法範例顯示了一些與許多強型別參數結合使用的環繞 Advice
-
Java
-
Kotlin
package com.xyz.service;
public interface PersonService {
Person getPerson(String personName, int age);
}
public class DefaultPersonService implements PersonService {
public Person getPerson(String name, int age) {
return new Person(name, age);
}
}
package com.xyz.service
interface PersonService {
fun getPerson(personName: String, age: Int): Person
}
class DefaultPersonService : PersonService {
fun getPerson(name: String, age: Int): Person {
return Person(name, age)
}
}
接下來是 Aspect。請注意,profile(..)
方法接受許多強型別參數,其中第一個參數恰好是用於繼續方法調用的連接點。此參數的存在表示 profile(..)
將用作 around
Advice,如下例所示
-
Java
-
Kotlin
package com.xyz;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class SimpleProfiler {
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}
package com.xyz
import org.aspectj.lang.ProceedingJoinPoint
import org.springframework.util.StopWatch
class SimpleProfiler {
fun profile(call: ProceedingJoinPoint, name: String, age: Int): Any? {
val clock = StopWatch("Profiling for '$name' and '$age'")
try {
clock.start(call.toShortString())
return call.proceed()
} finally {
clock.stop()
println(clock.prettyPrint())
}
}
}
最後,以下範例 XML 配置會影響特定連接點的先前 Advice 的執行
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="personService" class="com.xyz.service.DefaultPersonService"/>
<!-- this is the actual advice itself -->
<bean id="profiler" class="com.xyz.SimpleProfiler"/>
<aop:config>
<aop:aspect ref="profiler">
<aop:pointcut id="theExecutionOfSomePersonServiceMethod"
expression="execution(* com.xyz.service.PersonService.getPerson(String,int))
and args(name, age)"/>
<aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
method="profile"/>
</aop:aspect>
</aop:config>
</beans>
請考慮以下驅動程式 Script
-
Java
-
Kotlin
public class Boot {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
PersonService person = ctx.getBean(PersonService.class);
person.getPerson("Pengo", 12);
}
}
fun main() {
val ctx = ClassPathXmlApplicationContext("beans.xml")
val person = ctx.getBean(PersonService.class)
person.getPerson("Pengo", 12)
}
使用這樣的 Boot
類別,我們將在標準輸出上獲得類似於以下的輸出
StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0 ----------------------------------------- ms % Task name ----------------------------------------- 00000 ? execution(getFoo)
Advice 順序
當多個 Advice 需要在同一連接點(執行方法)執行時,排序規則如Advice 順序中所述。Aspect 之間的優先順序透過 <aop:aspect>
元素中的 order
屬性或透過將 @Order
註解新增至支援 Aspect 的 Bean 或透過讓 Bean 實作 Ordered
介面來確定。
與在同一個 例如,如果給定一個 作為一般經驗法則,如果您發現您在同一個 |
引介
引介(在 AspectJ 中稱為跨類型宣告)讓 Aspect 宣告 Advised 物件實作給定的介面,並代表這些物件提供該介面的實作。
您可以使用 aop:aspect
元素內的 aop:declare-parents
元素來進行介紹。您可以使用 aop:declare-parents
元素來宣告符合的類型具有新的父類別 (因此得名)。例如,給定一個名為 UsageTracked
的介面和一個該介面的實作名為 DefaultUsageTracked
,以下的 aspect 宣告所有服務介面的實作類別也實作 UsageTracked
介面。(例如,為了透過 JMX 暴露統計資訊。)
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xyz.service.*+"
implement-interface="com.xyz.service.tracking.UsageTracked"
default-impl="com.xyz.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="execution(* com.xyz..service.*.*(..))
and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
支援 usageTracking
bean 的類別接著會包含以下方法
-
Java
-
Kotlin
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
}
要實作的介面由 implement-interface
屬性決定。types-matching
屬性的值是一個 AspectJ 類型模式。任何符合類型的 bean 都會實作 UsageTracked
介面。請注意,在前述範例的 before advice 中,服務 bean 可以直接作為 UsageTracked
介面的實作。若要以程式化的方式存取 bean,您可以撰寫如下程式碼
-
Java
-
Kotlin
UsageTracked usageTracked = context.getBean("myService", UsageTracked.class);
val usageTracked = context.getBean("myService", UsageTracked.class)
Advisors(通知器)
「Advisors」的概念來自 Spring 中定義的 AOP 支援,在 AspectJ 中沒有直接對應的概念。Advisor 就像是一個小的、自我包含的 aspect,它只有單一一個 advice。advice 本身由 bean 表示,且必須實作 Spring 中的 Advice 類型 中描述的 advice 介面之一。Advisors 可以利用 AspectJ pointcut 表達式。
Spring 透過 <aop:advisor>
元素支援 advisor 的概念。您最常看到它與事務性 advice 一起使用,事務性 advice 在 Spring 中也有自己的命名空間支援。以下範例展示了一個 advisor
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="tx-advice" />
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
除了前述範例中使用的 pointcut-ref
屬性之外,您也可以使用 pointcut
屬性來內聯定義 pointcut 表達式。
為了定義 advisor 的優先順序,以便 advice 可以參與排序,請使用 order
屬性來定義 advisor 的 Ordered
值。
AOP Schema 範例
本節展示了使用 schema 支援重寫後,AOP 範例 中的並行鎖定失敗重試範例看起來會是什麼樣子。
業務服務的執行有時會因為並行問題而失敗 (例如,死鎖失敗者)。如果操作被重試,則很可能在下一次嘗試時成功。對於在這種情況下適合重試的業務服務 (不需要回到使用者以解決衝突的冪等操作),我們希望透明地重試操作,以避免用戶端看到 PessimisticLockingFailureException
。這是一個明顯跨越多個服務層服務的需求,因此非常適合透過 aspect 來實作。
因為我們想要重試操作,所以我們需要使用 around advice,以便我們可以多次呼叫 proceed
。以下列表顯示了基本的 aspect 實作 (這是一個使用 schema 支援的常規 Java 類別)
-
Java
-
Kotlin
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
class ConcurrentOperationExecutor : Ordered {
private val DEFAULT_MAX_RETRIES = 2
private var maxRetries = DEFAULT_MAX_RETRIES
private var order = 1
fun setMaxRetries(maxRetries: Int) {
this.maxRetries = maxRetries
}
override fun getOrder(): Int {
return this.order
}
fun setOrder(order: Int) {
this.order = order
}
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any? {
var numAttempts = 0
var lockFailureException: PessimisticLockingFailureException
do {
numAttempts++
try {
return pjp.proceed()
} catch (ex: PessimisticLockingFailureException) {
lockFailureException = ex
}
} while (numAttempts <= this.maxRetries)
throw lockFailureException
}
}
請注意,aspect 實作了 Ordered
介面,以便我們可以將 aspect 的優先順序設定高於事務 advice (我們希望每次重試都有一個新的事務)。maxRetries
和 order
屬性都由 Spring 配置。主要的操作發生在 doConcurrentOperation
around advice 方法中。我們嘗試繼續執行。如果我們因 PessimisticLockingFailureException
而失敗,我們會再次嘗試,除非我們已經用盡所有的重試次數。
這個類別與 @AspectJ 範例中使用的類別相同,但註解已被移除。 |
相應的 Spring 配置如下
<aop:config>
<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..))"/>
<aop:around
pointcut-ref="idempotentOperation"
method="doConcurrentOperation"/>
</aop:aspect>
</aop:config>
<bean id="concurrentOperationExecutor"
class="com.xyz.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
請注意,暫時我們假設所有業務服務都是冪等的。如果情況並非如此,我們可以改進 aspect,使其僅重試真正冪等的操作,透過引入 Idempotent
註解並使用該註解來註釋服務操作的實作,如下範例所示
-
Java
-
Kotlin
@Retention(RetentionPolicy.RUNTIME)
// marker annotation
public @interface Idempotent {
}
@Retention(AnnotationRetention.RUNTIME)
// marker annotation
annotation class Idempotent
將 aspect 更改為僅重試冪等操作,涉及到改進 pointcut 表達式,以便僅匹配 @Idempotent
操作,如下所示
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..)) and
@annotation(com.xyz.service.Idempotent)"/>