基於 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.*.*(..)) &amp;&amp; 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 子運算式時,&amp;&amp; 在 XML 文件中顯得笨拙,因此您可以使用 andornot 關鍵字來代替 &amp;&amp;||!。例如,先前的 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>) 層級定義的具名 Pointcutid(請參閱宣告 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 介面來確定。

與在同一個 @Aspect 類別中定義的 Advice 方法的優先順序規則相反,當在同一個 <aop:aspect> 元素中定義的兩個 Advice 都需要在同一個連接點執行時,優先順序由 Advice 元素在封閉的 <aop:aspect> 元素中宣告的順序決定,從最高優先順序到最低優先順序。

例如,如果給定一個 around Advice 和一個 before Advice 在同一個 <aop:aspect> 元素中定義,它們應用於同一個連接點,為了確保 around Advice 比 before Advice 具有更高的優先順序,則必須在 <aop:before> 元素之前宣告 <aop:around> 元素。

作為一般經驗法則,如果您發現您在同一個 <aop:aspect> 元素中定義了多個 Advice,它們應用於同一個連接點,請考慮將此類 Advice 方法摺疊為每個 <aop:aspect> 元素中每個連接點的一個 Advice 方法,或將 Advice 片段重構為您可以 Aspect 層級排序的單獨 <aop:aspect> 元素。

引介

引介(在 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)

Aspect 實例化模型

schema 定義的 aspect 唯一支援的實例化模型是 singleton 模型。未來的版本可能會支援其他實例化模型。

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 (我們希望每次重試都有一個新的事務)。maxRetriesorder 屬性都由 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)"/>