基於 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.*.*(..)) &amp;&amp; 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 文件中使用 &amp;&amp; 不方便,因此您可以使用 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 型別,它們具有完全相同的語義。

前置 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>)的 命名 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 方法。

後置返回 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 介面來確定。

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

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

一般來說,如果您發現在同一 <aop:aspect> 元素中定義了多個適用於同一連線點的 Advice,請考慮將這些 Advice 方法合併到每個 <aop:aspect> 元素中每個連線點的一個 Advice 方法中,或者將這些 Advice 重構為單獨的 <aop:aspect> 元素,以便您可以在 Aspect 級別進行排序。

引入 (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)

Aspect 例項化模型

Schema 定義的 Aspect 唯一支援的例項化模型是單例模型。其他例項化模型可能會在將來的版本中支援。

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 介面,以便我們可以將切面的優先順序設定得高於事務通知(我們希望每次重試時都有一個新的事務)。maxRetriesorder 屬性都由 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)"/>