基於 Schema 的 AOP 支援
如果您更喜歡基於 XML 的格式,Spring 也支援使用 aop 名稱空間標籤定義切面。支援與使用 @AspectJ 樣式完全相同的切入點表示式和通知型別。因此,在本節中,我們重點介紹這種語法,並請讀者參考上一節(@AspectJ 支援)的討論,以瞭解如何編寫切入點表示式和通知引數的繫結。
要使用本節中描述的 aop 名稱空間標籤,您需要匯入 spring-aop schema,如 基於 XML Schema 的配置 中所述。有關如何在 aop 名稱空間中匯入標籤的資訊,請參閱 AOP schema。
在您的 Spring 配置中,所有切面和顧問元素都必須放在 <aop:config> 元素中(您可以在一個應用程式上下文配置中擁有多個 <aop:config> 元素)。<aop:config> 元素可以包含切入點、顧問和切面元素(請注意,這些元素必須按此順序宣告)。
<aop:config> 樣式配置大量使用了 Spring 的 自動代理 機制。如果您已經透過使用 BeanNameAutoProxyCreator 或類似方式顯式使用了自動代理,這可能會導致問題(例如通知未織入)。推薦的使用模式是隻使用 <aop:config> 樣式或只使用 AutoProxyCreator 樣式,切勿混用。 |
宣告切面
當您使用 schema 支援時,切面是一個常規的 Java 物件,在您的 Spring 應用程式上下文中定義為一個 bean。狀態和行為捕獲在物件的欄位和方法中,而切入點和通知資訊則捕獲在 XML 中。
您可以使用 <aop:aspect> 元素宣告切面,並透過使用 ref 屬性引用支援 bean,如下例所示
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
支援切面(本例中為 aBean)的 bean 當然可以像任何其他 Spring bean 一樣進行配置和依賴注入。
宣告切入點
您可以在 <aop:config> 元素中宣告一個_命名切入點_,從而允許在多個切面和顧問之間共享切入點定義。
表示服務層中任何業務服務執行的切入點可以定義如下
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))" />
</aop:config>
請注意,切入點表示式本身使用與 @AspectJ 支援 中描述的相同的 AspectJ 切入點表示式語言。如果使用基於 schema 的宣告樣式,您還可以引用 @Aspect 型別中定義的_命名切入點_,在切入點表示式中。因此,定義上述切入點的另一種方法如下所示
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.CommonPointcuts.businessService()" /> (1)
</aop:config>
| 1 | 引用 共享命名切入點定義 中定義的 businessService 命名切入點。 |
在切面_內部_宣告切入點與宣告頂級切入點非常相似,如下例所示
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
與 @AspectJ 切面非常相似,透過基於 schema 的定義樣式宣告的切入點可以收集連線點上下文。例如,以下切入點將 this 物件作為連線點上下文並將其傳遞給通知
<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>
通知必須透過包含具有匹配名稱的引數來宣告以接收收集到的連線點上下文,如下所示
-
Java
-
Kotlin
public void monitor(Object service) {
// ...
}
fun monitor(service: Any) {
// ...
}
當組合切入點子表示式時,&& 在 XML 文件中很不方便,因此您可以使用 and、or 和 not 關鍵字分別代替 &&、|| 和 !。例如,前面的切入點可以更好地寫成如下
<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>
請注意,以這種方式定義的切入點透過其 XML id 引用,不能用作命名切入點來形成複合切入點。因此,基於 schema 的定義樣式中的命名切入點支援比 @AspectJ 樣式提供的更為有限。
宣告通知
基於 schema 的 AOP 支援使用與 @AspectJ 樣式相同的五種通知,並且它們具有完全相同的語義。
前置通知
前置通知在匹配方法執行之前執行。它在 <aop:aspect> 中透過使用 <aop:before> 元素宣告,如下例所示
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
在上面的例子中,dataAccessOperation 是在頂層(<aop:config>)定義的_命名切入點_的 id(參見 宣告切入點)。
| 正如我們在討論 @AspectJ 樣式時所指出的,使用_命名切入點_可以顯著提高程式碼的可讀性。有關詳細資訊,請參閱 共享命名切入點定義。 |
要以內聯方式定義切入點,請將 pointcut-ref 屬性替換為 pointcut 屬性,如下所示
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
method 屬性標識提供通知主體的方法(doAccessCheck)。此方法必須為包含通知的切面元素引用的 bean 定義。在執行資料訪問操作(與切入點表示式匹配的方法執行連線點)之前,將呼叫切面 bean 上的 doAccessCheck 方法。
後置返回通知
後置返回通知在匹配方法執行正常完成時執行。它以與前置通知相同的方式在 <aop:aspect> 中宣告。以下示例顯示瞭如何宣告它
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
與 @AspectJ 樣式一樣,您可以在通知主體中獲取返回值。為此,請使用 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 樣式一樣,您可以在通知主體中獲取丟擲的異常。為此,請使用 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>
環繞通知
最後一種通知是_環繞_通知。環繞通知在匹配方法的執行“周圍”執行。它有機會在方法執行之前和之後進行工作,並確定方法何時、如何以及是否實際執行。如果您需要以執行緒安全的方式在方法執行之前和之後共享狀態(例如,啟動和停止計時器),通常會使用環繞通知。
|
始終使用滿足您要求的功能最弱的通知形式。 例如,如果前置通知足以滿足您的需求,請不要使用_環繞_通知。 |
您可以使用 aop:around 元素宣告環繞通知。通知方法應將 Object 宣告為其返回型別,並且方法的第一個引數必須是 ProceedingJoinPoint 型別。在通知方法的主體中,您必須在 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 通知器的實現可以與 @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()
}
通知引數
基於 schema 的宣告樣式支援與 @AspectJ 支援相同的方式進行完全型別化的通知——透過按名稱匹配切入點引數與通知方法引數。有關詳細資訊,請參閱 通知引數。如果您希望顯式指定通知方法的引數名稱(不依賴前面描述的檢測策略),您可以透過使用通知元素的 arg-names 屬性來實現,該屬性以與通知註解中的 argNames 屬性相同的方式處理(如 確定引數名稱 中所述)。以下示例顯示瞭如何在 XML 中指定引數名稱
<aop:before
pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)" (1)
method="audit"
arg-names="auditable" />
| 1 | 引用 組合切入點表示式 中定義的 publicMethod 命名切入點。 |
arg-names 屬性接受逗號分隔的引數名稱列表。
以下是基於 XSD 方法的一個稍微複雜一點的示例,它展示了一些與多個強型別引數結合使用的環繞通知
-
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)
}
}
接下來是切面。請注意 profile(..) 方法接受多個強型別引數的事實,其中第一個引數恰好是用於繼續方法呼叫的連線點。此引數的存在表明 profile(..) 將用作 around 通知,如下例所示
-
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 配置示例實現了特定連線點的前述通知的執行
<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)
通知排序
當多個通知需要在同一個連線點(執行方法)上執行時,排序規則如 通知排序 中所述。切面之間的優先順序透過 <aop:aspect> 元素中的 order 屬性或透過向支援切面的 bean 新增 @Order 註解或透過讓 bean 實現 Ordered 介面來確定。
|
與在同一個 例如,給定一個在同一個 根據經驗法則,如果您發現在同一個 |
引入
引入(在 AspectJ 中稱為交叉型別宣告)允許切面宣告被通知物件實現給定介面,並代表這些物件提供該介面的實現。
您可以使用 aop:declare-parents 元素在 aop:aspect 中進行引入。您可以使用 aop:declare-parents 元素宣告匹配型別具有新的父級(因此得名)。例如,給定一個名為 UsageTracked 的介面和一個名為 DefaultUsageTracked 的該介面的實現,以下切面宣告所有服務介面的實現者也實現 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 介面。請注意,在前面示例的前置通知中,服務 bean 可以直接用作 UsageTracked 介面的實現。要以程式設計方式訪問 bean,您可以編寫如下程式碼
-
Java
-
Kotlin
UsageTracked usageTracked = context.getBean("myService", UsageTracked.class);
val usageTracked = context.getBean("myService", UsageTracked.class)
顧問
“顧問”的概念來自 Spring 中定義的 AOP 支援,在 AspectJ 中沒有直接的等價物。顧問就像一個小的、自包含的切面,它只有一段通知。通知本身由一個 bean 表示,並且必須實現 Spring 中的通知型別 中描述的一個通知介面。顧問可以利用 AspectJ 切入點表示式。
Spring 透過 <aop: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)"/>