基於 Schema 的 AOP 支援
如果您更喜歡基於 XML 的格式,Spring 也支援使用 aop
名稱空間標籤定義 Aspect。它支援與 @AspectJ 風格完全相同的 Pointcut 表示式和 Advice 型別。因此,在本節中,我們重點介紹該語法,關於編寫 Pointcut 表示式和繫結 Advice 引數的理解,請讀者參閱上一節(@AspectJ 支援)的討論。
要使用本節描述的 aop 名稱空間標籤,您需要匯入 spring-aop
schema,具體描述請參見基於 XML Schema 的配置。有關如何在 aop
名稱空間中匯入標籤的資訊,請參見AOP schema。
在您的 Spring 配置中,所有 aspect 和 advisor 元素都必須放置在 <aop:config>
元素內(在應用程式上下文配置中可以有多個 <aop:config>
元素)。<aop:config>
元素可以包含 pointcut、advisor 和 aspect 元素(注意,這些元素必須按此順序宣告)。
<aop:config> 風格的配置大量使用了 Spring 的自動代理機制。如果您已經透過使用 BeanNameAutoProxyCreator 或類似的方式使用了顯式自動代理,這可能會導致問題(例如 Advice 未被織入)。推薦的使用模式是隻使用 <aop:config> 風格或只使用 AutoProxyCreator 風格,切勿混合使用。 |
宣告一個 Aspect
當您使用 Schema 支援時,Aspect 是在 Spring 應用程式上下文中定義為 Bean 的常規 Java 物件。狀態和行為在物件的欄位和方法中捕獲,而 Pointcut 和 Advice 資訊則在 XML 中捕獲。
您可以使用 <aop:aspect>
元素宣告一個 Aspect,並使用 ref
屬性引用 backing 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 可以收集連線點上下文。例如,以下 Pointcut 將 this
物件收集為連線點上下文並將其傳遞給 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 必須宣告為透過包含匹配名稱的引數來接收收集到的連線點上下文,如下所示:
-
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 型別,它們具有完全相同的語義。
前置 Advice (Before Advice)
前置 Advice 在匹配的方法執行之前執行。它使用 <aop:before>
元素在 <aop:aspect>
內部宣告,如下例所示:
<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
方法。
後置返回 Advice (After Returning Advice)
後置返回 Advice 在匹配的方法執行正常完成時執行。它在 <aop:aspect>
內部宣告,方式與前置 Advice 相同。以下示例顯示如何宣告它:
<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) {...
後置丟擲 Advice (After Throwing Advice)
後置丟擲 Advice 在匹配的方法執行因丟擲異常而退出時執行。它使用 after-throwing
元素在 <aop:aspect>
內部宣告,如下例所示:
<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) {...
後置 (Finally) Advice
後置 (Finally) Advice 無論匹配的方法執行如何退出都會執行。您可以使用 after
元素宣告它,如下例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doReleaseLock"/>
...
</aop:aspect>
環繞 Advice (Around Advice)
最後一種 Advice 型別是 環繞 Advice。環繞 Advice 在匹配方法的執行“周圍”執行。它有機會在方法執行之前和之後執行工作,並決定方法何時、如何以及是否實際執行。如果您需要在方法執行之前和之後以執行緒安全的方式共享狀態(例如,啟動和停止計時器),通常會使用環繞 Advice。
始終使用滿足您需求的最低許可權 Advice 形式。 例如,如果 前置 Advice 足以滿足您的需求,請不要使用 環繞 Advice。 |
您可以使用 aop:around
元素宣告環繞 Advice。Advice 方法應宣告 Object
作為其返回型別,並且該方法的第一個引數必須是 ProceedingJoinPoint
型別。在 Advice 方法的主體內,您必須在 ProceedingJoinPoint
上呼叫 proceed()
以便基礎方法執行。不帶引數呼叫 proceed()
將導致在呼叫基礎方法時提供呼叫者的原始引數。對於高階用例,proceed()
方法有一個過載變體,它接受一個引數陣列(Object[]
)。陣列中的值將用作呼叫基礎方法時的引數。有關呼叫帶有 Object[]
引數的 proceed
的注意事項,請參閱環繞 Advice。
以下示例顯示如何在 XML 中宣告環繞 Advice:
<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>
考慮以下驅動指令碼:
-
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
屬性確定,或者透過向支援 Aspect 的 Bean 新增 @Order
註解,或者透過讓 Bean 實現 Ordered
介面來確定。
與在同一 例如,給定在同一 一般來說,如果您發現在同一 |
引入 (Introductions)
引入(在 AspectJ 中稱為 inter-type declarations)允許 Aspect 宣告被通知的物件實現給定的介面,並代表這些物件提供該介面的實現。
您可以使用 aop:declare-parents
元素在 aop:aspect
內部進行引入。您可以使用 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
介面。請注意,在前面示例的前置 Advice 中,服務 Bean 可以直接用作 UsageTracked
介面的實現。要以程式設計方式訪問 Bean,您可以編寫以下程式碼:
-
Java
-
Kotlin
UsageTracked usageTracked = context.getBean("myService", UsageTracked.class);
val usageTracked = context.getBean("myService", UsageTracked.class)
Advisors
“Advisor”的概念來自 Spring 中定義的 AOP 支援,在 AspectJ 中沒有直接對應的概念。Advisor 就像一個小的自包含的 Aspect,它包含一個單獨的 Advice。Advice 本身由一個 Bean 表示,並且必須實現Spring 中的 Advice 型別中描述的一個 Advice 介面。Advisor 可以利用 AspectJ Pointcut 表示式。
Spring 透過 <aop:advisor>
元素支援 Advisor(通知器)概念。您最常見的使用場景是與事務通知結合,事務通知在 Spring 中也有自己的名稱空間支援。以下示例展示了一個通知器
<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
屬性內聯定義一個切入點表示式。
要定義通知器的優先順序,以便通知可以參與排序,請使用 order
屬性來定義通知器的 Ordered
值。
一個 AOP Schema 示例
本節展示了來自 一個 AOP 示例 的併發鎖定失敗重試示例在用 Schema 支援重寫後的樣子。
業務服務的執行有時會因為併發問題(例如,死鎖失敗)而失敗。如果操作被重試,很可能在下一次嘗試中成功。對於在這些條件下適合重試的業務服務(無需返回給使用者解決衝突的冪等操作),我們希望透明地重試操作,以避免客戶端看到 PessimisticLockingFailureException
。這是一個明顯跨越服務層中多個服務的需求,因此非常適合透過切面來實現。
因為我們想重試操作,所以需要使用環繞通知,這樣我們就可以多次呼叫 proceed
。以下清單顯示了基本的切面實現(它是一個使用 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
}
}
請注意,該切面實現了 Ordered
介面,以便我們可以將切面的優先順序設定得高於事務通知(我們希望每次重試時都有一個新的事務)。maxRetries
和 order
屬性都由 Spring 配置。主要操作發生在 doConcurrentOperation
環繞通知方法中。我們嘗試繼續執行。如果因 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>
請注意,目前我們假設所有業務服務都是冪等的。如果不是這種情況,我們可以細化切面,使其僅重試真正冪等的操作,方法是引入 Idempotent
註解並使用該註解來標註服務操作的實現,如下例所示
-
Java
-
Kotlin
@Retention(RetentionPolicy.RUNTIME)
// marker annotation
public @interface Idempotent {
}
@Retention(AnnotationRetention.RUNTIME)
// marker annotation
annotation class Idempotent
要修改切面以僅重試冪等操作,需要細化切入點表示式,使其僅匹配 @Idempotent
操作,如下所示
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..)) and
@annotation(com.xyz.service.Idempotent)"/>