宣告建議 (Advice)

建議 (Advice) 與切入點運算式相關聯,並在切入點匹配的方法執行之前、之後或周圍執行。切入點運算式可以是內聯切入點或對具名切入點的參考。

前置建議 (Before Advice)

您可以使用 @Before 註解在 Aspect 中宣告前置建議 (before advice)。

以下範例使用內聯切入點運算式。

  • Java

  • Kotlin

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

	@Before("execution(* com.xyz.dao.*.*(..))")
	public void doAccessCheck() {
		// ...
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before

@Aspect
class BeforeExample {

	@Before("execution(* com.xyz.dao.*.*(..))")
	fun doAccessCheck() {
		// ...
	}
}

如果我們使用具名切入點,我們可以將前面的範例重寫如下

  • Java

  • Kotlin

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

	@Before("com.xyz.CommonPointcuts.dataAccessOperation()")
	public void doAccessCheck() {
		// ...
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before

@Aspect
class BeforeExample {

	@Before("com.xyz.CommonPointcuts.dataAccessOperation()")
	fun doAccessCheck() {
		// ...
	}
}

後置返回建議 (After Returning Advice)

後置返回建議 (after returning advice) 在匹配的方法執行正常返回時執行。您可以使用 @AfterReturning 註解宣告它。

  • Java

  • Kotlin

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

	@AfterReturning("execution(* com.xyz.dao.*.*(..))")
	public void doAccessCheck() {
		// ...
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning

@Aspect
class AfterReturningExample {

	@AfterReturning("execution(* com.xyz.dao.*.*(..))")
	fun doAccessCheck() {
		// ...
	}
}
您可以擁有多個建議 (advice) 宣告(以及其他成員),所有這些都在同一個 Aspect 內部。在這些範例中,我們僅顯示單個建議宣告,以專注於每個建議的效果。

有時,您需要在建議 (advice) 主體中存取實際的傳回值。您可以使用 @AfterReturning 的表單,將傳回值繫結以取得該存取權,如下列範例所示

  • Java

  • Kotlin

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

	@AfterReturning(
		pointcut="execution(* com.xyz.dao.*.*(..))",
		returning="retVal")
	public void doAccessCheck(Object retVal) {
		// ...
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning

@Aspect
class AfterReturningExample {

	@AfterReturning(
		pointcut = "execution(* com.xyz.dao.*.*(..))",
		returning = "retVal")
	fun doAccessCheck(retVal: Any?) {
		// ...
	}
}

returning 屬性中使用的名稱必須與建議方法中參數的名稱相對應。當方法執行返回時,傳回值會作為對應的引數值傳遞給建議方法。 returning 子句也將匹配限制為僅那些傳回指定類型值的方法執行(在本例中為 Object,它匹配任何傳回值)。

請注意,使用後置返回建議 (after returning advice) 時,無法傳回完全不同的參考。

後置拋出建議 (After Throwing Advice)

後置拋出建議 (after throwing advice) 在匹配的方法執行透過拋出例外退出時執行。您可以使用 @AfterThrowing 註解宣告它,如下列範例所示

  • Java

  • Kotlin

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

	@AfterThrowing("execution(* com.xyz.dao.*.*(..))")
	public void doRecoveryActions() {
		// ...
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing

@Aspect
class AfterThrowingExample {

	@AfterThrowing("execution(* com.xyz.dao.*.*(..))")
	fun doRecoveryActions() {
		// ...
	}
}

通常,您希望建議僅在拋出給定類型的例外時執行,並且您也經常需要在建議主體中存取拋出的例外。您可以使用 throwing 屬性來限制匹配(如果需要 - 否則使用 Throwable 作為例外類型)並將拋出的例外繫結到建議參數。以下範例顯示如何執行此操作

  • Java

  • Kotlin

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

	@AfterThrowing(
		pointcut="execution(* com.xyz.dao.*.*(..))",
		throwing="ex")
	public void doRecoveryActions(DataAccessException ex) {
		// ...
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing

@Aspect
class AfterThrowingExample {

	@AfterThrowing(
		pointcut = "execution(* com.xyz.dao.*.*(..))",
		throwing = "ex")
	fun doRecoveryActions(ex: DataAccessException) {
		// ...
	}
}

throwing 屬性中使用的名稱必須與建議方法中參數的名稱相對應。當方法執行透過拋出例外退出時,例外會作為對應的引數值傳遞給建議方法。 throwing 子句也將匹配限制為僅那些拋出指定類型例外的方法執行(在本例中為 DataAccessException)。

請注意,@AfterThrowing 不表示一般例外處理回呼。具體來說,@AfterThrowing 建議方法僅應接收來自連接點(使用者宣告的目標方法)本身的例外,而不是來自隨附的 @After/@AfterReturning 方法的例外。

後置 (Finally) 建議 (After (Finally) Advice)

後置 (finally) 建議 (after (finally) advice) 在匹配的方法執行退出時執行。它使用 @After 註解宣告。後置建議必須準備好處理正常和例外返回條件。它通常用於釋放資源和類似目的。以下範例顯示如何使用後置 finally 建議

  • Java

  • Kotlin

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

	@After("execution(* com.xyz.dao.*.*(..))")
	public void doReleaseLock() {
		// ...
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.After

@Aspect
class AfterFinallyExample {

	@After("execution(* com.xyz.dao.*.*(..))")
	fun doReleaseLock() {
		// ...
	}
}

請注意,AspectJ 中的 @After 建議定義為「後置 finally 建議 (after finally advice)」,類似於 try-catch 語句中的 finally 區塊。它將針對任何結果(正常返回或從連接點(使用者宣告的目標方法)拋出的例外)調用,這與僅適用於成功正常返回的 @AfterReturning 相反。

環繞建議 (Around Advice)

最後一種建議是環繞建議 (around advice)。環繞建議「圍繞」匹配方法的執行執行。它有機會在方法執行之前和之後執行工作,並確定方法何時、如何甚至是否實際執行。如果您需要在方法執行之前和之後以執行緒安全的方式共享狀態(例如,啟動和停止計時器),則通常使用環繞建議。

始終使用滿足您需求的最弱形式的建議。

例如,如果前置建議 (before advice) 足以滿足您的需求,則不要使用環繞建議 (around advice)。

環繞建議 (around advice) 透過使用 @Around 註解註解方法來宣告。該方法應將 Object 宣告為其返回類型,並且該方法的第一個參數必須是 ProceedingJoinPoint 類型。在建議方法的主體中,您必須在 ProceedingJoinPoint 上調用 proceed(),以便執行底層方法。調用不帶引數的 proceed() 將導致在調用底層方法時將呼叫者的原始引數提供給底層方法。對於進階用例,proceed() 方法有一個重載變體,它接受引數陣列 (Object[])。陣列中的值將在調用底層方法時用作底層方法的引數。

當使用 Object[] 調用時,proceed 的行為與 AspectJ 編譯器編譯的環繞建議 (around advice) 的 proceed 行為略有不同。對於使用傳統 AspectJ 語言編寫的環繞建議,傳遞給 proceed 的引數數量必須與傳遞給環繞建議的引數數量(而不是底層連接點採用的引數數量)相符,並且在給定引數位置傳遞給 proceed 的值取代了連接點中實體的原始值,該值繫結到該實體(如果這現在沒有意義,請不要擔心)。

Spring 採用的方法更簡單,並且更符合其基於 Proxy、僅執行的語意。如果您編譯為 Spring 撰寫的 @AspectJ Aspect 並將 proceed 與 AspectJ 編譯器和織入器一起使用引數,則只需注意此差異。有一種方法可以編寫此類 Aspect,這些 Aspect 在 Spring AOP 和 AspectJ 之間 100% 相容,這在以下關於建議參數的章節中討論。

環繞建議 (around advice) 傳回的值是方法呼叫者看到的傳回值。例如,一個簡單的快取 Aspect 如果有值可以從快取中傳回值,或者如果沒有值,則調用 proceed()(並傳回該值)。請注意,可以在環繞建議 (around advice) 的主體中調用 proceed 一次、多次或根本不調用。所有這些都是合法的。

如果您將環繞建議 (around advice) 方法的傳回類型宣告為 void,則始終會將 null 傳回給呼叫者,實際上忽略了任何 proceed() 調用的結果。因此,建議環繞建議 (around advice) 方法宣告 Object 的傳回類型。建議方法通常應傳回從 proceed() 調用傳回的值,即使底層方法具有 void 傳回類型也是如此。但是,建議可以選擇性地傳回快取值、包裝值或取決於用例的其他值。

以下範例顯示如何使用環繞建議 (around advice)

  • Java

  • Kotlin

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

	@Around("execution(* com.xyz..service.*.*(..))")
	public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
		// start stopwatch
		Object retVal = pjp.proceed();
		// stop stopwatch
		return retVal;
	}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.ProceedingJoinPoint

@Aspect
class AroundExample {

	@Around("execution(* com.xyz..service.*.*(..))")
	fun doBasicProfiling(pjp: ProceedingJoinPoint): Any? {
		// start stopwatch
		val retVal = pjp.proceed()
		// stop stopwatch
		return retVal
	}
}

建議參數 (Advice Parameters)

Spring 提供完全類型化的建議 (advice),這表示您可以在建議簽名中宣告您需要的參數(正如我們在前面看到的返回和拋出範例),而不是一直使用 Object[] 陣列。我們在本節稍後會看到如何使引數和其他上下文值可供建議主體使用。首先,我們來看看如何編寫可以找出建議目前正在建議的方法的通用建議。

存取目前的 JoinPoint

任何建議方法都可以宣告一個 org.aspectj.lang.JoinPoint 類型的參數作為其第一個參數。請注意,環繞建議 (around advice) 需要宣告 ProceedingJoinPoint 類型的第一個參數,它是 JoinPoint 的子類別。

JoinPoint 介面提供許多有用的方法

  • getArgs():傳回方法引數。

  • getThis():傳回 Proxy 物件。

  • getTarget():傳回目標物件。

  • getSignature():傳回正在建議的方法的描述。

  • toString():印出正在建議的方法的有用描述。

請參閱javadoc 以取得更多詳細資訊。

將參數傳遞給建議 (Advice)

我們已經看到了如何繫結傳回值或例外值(使用後置返回和後置拋出建議)。要使引數值可供建議主體使用,您可以使用 args 的繫結形式。如果您在 args 運算式中使用參數名稱而不是類型名稱,則在調用建議時,對應引數的值將作為參數值傳遞。一個範例應該可以使這更清楚。假設您要建議執行將 Account 物件作為第一個參數的 DAO 操作,並且您需要在建議主體中存取帳戶。您可以編寫以下內容

  • Java

  • Kotlin

@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
public void validateAccount(Account account) {
	// ...
}
@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
fun validateAccount(account: Account) {
	// ...
}

切入點運算式的 args(account,..) 部分有兩個用途。首先,它將匹配限制為僅那些方法執行,其中方法至少採用一個參數,並且傳遞給該參數的引數是 Account 的實例。其次,它透過 account 參數使實際的 Account 物件可供建議使用。

另一種編寫此程式碼的方式是宣告一個切入點,該切入點在匹配連接點時「提供」Account 物件值,然後從建議中參考具名切入點。它看起來如下所示

  • Java

  • Kotlin

@Pointcut("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
	// ...
}
@Pointcut("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
private fun accountDataAccessOperation(account: Account) {
}

@Before("accountDataAccessOperation(account)")
fun validateAccount(account: Account) {
	// ...
}

請參閱 AspectJ 程式設計指南以取得更多詳細資訊。

Proxy 物件 (this)、目標物件 (target) 和註解 (@within@target@annotation@args) 都可以以類似的方式繫結。下一組範例顯示如何匹配使用 @Auditable 註解註解的方法的執行並提取稽核代碼

以下顯示 @Auditable 註解的定義

  • Java

  • Kotlin

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
	AuditCode value();
}
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Auditable(val value: AuditCode)

以下顯示匹配 @Auditable 方法執行的建議

  • Java

  • Kotlin

@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") (1)
public void audit(Auditable auditable) {
	AuditCode code = auditable.value();
	// ...
}
1 參考組合切入點運算式中定義的 publicMethod 具名切入點。
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") (1)
fun audit(auditable: Auditable) {
	val code = auditable.value()
	// ...
}
1 參考組合切入點運算式中定義的 publicMethod 具名切入點。

建議參數和泛型 (Advice Parameters and Generics)

Spring AOP 可以處理類別宣告和方法參數中使用的泛型。假設您有一個類似於以下的泛型類型

  • Java

  • Kotlin

public interface Sample<T> {
	void sampleGenericMethod(T param);
	void sampleGenericCollectionMethod(Collection<T> param);
}
interface Sample<T> {
	fun sampleGenericMethod(param: T)
	fun sampleGenericCollectionMethod(param: Collection<T>)
}

您可以透過將建議參數繫結到您要攔截方法的方法參數類型,將方法類型的攔截限制為某些參數類型

  • Java

  • Kotlin

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
	// Advice implementation
}
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
fun beforeSampleMethod(param: MyType) {
	// Advice implementation
}

這種方法不適用於泛型集合。因此,您不能將切入點定義如下

  • Java

  • Kotlin

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
	// Advice implementation
}
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
fun beforeSampleMethod(param: Collection<MyType>) {
	// Advice implementation
}

為了讓這個功能運作,我們必須檢查集合中的每個元素,這是不合理的,因為我們也無法決定如何處理一般的 null 值。若要達到類似的效果,您必須將參數類型設定為 Collection<?>,並手動檢查元素的型別。

判斷引數名稱

通知調用中的參數綁定依賴於將切入點表達式中使用的名稱,與通知和切入點方法簽名中宣告的參數名稱進行匹配。

本節交替使用引數參數這兩個詞彙,因為 AspectJ API 將參數名稱稱為引數名稱。

Spring AOP 使用以下 ParameterNameDiscoverer 實作來判斷參數名稱。每個探索器都有機會探索參數名稱,而第一個成功的探索器獲勝。如果沒有已註冊的探索器能夠判斷參數名稱,則會拋出例外。

AspectJAnnotationParameterNameDiscoverer

使用使用者透過對應的通知或切入點註解中的 argNames 屬性明確指定的參數名稱。請參閱明確的引數名稱以了解詳細資訊。

KotlinReflectionParameterNameDiscoverer

使用 Kotlin 反射 API 來判斷參數名稱。只有當類別路徑上存在此類 API 時,才會使用此探索器。

StandardReflectionParameterNameDiscoverer

使用標準 java.lang.reflect.Parameter API 來判斷參數名稱。需要使用 javac-parameters 旗標編譯程式碼。建議在 Java 8 以上版本中使用此方法。

AspectJAdviceParameterNameDiscoverer

從切入點表達式、returningthrowing 子句推導參數名稱。請參閱 javadoc 以了解所用演算法的詳細資訊。

明確的引數名稱

@AspectJ 通知和切入點註解具有可選的 argNames 屬性,您可以使用它來指定註解方法的引數名稱。

如果 @AspectJ 切面已由 AspectJ 編譯器 (ajc) 編譯,即使沒有偵錯資訊,您也不需要新增 argNames 屬性,因為編譯器會保留所需的資訊。

同樣地,如果 @AspectJ 切面已使用 javac-parameters 旗標編譯,您也不需要新增 argNames 屬性,因為編譯器會保留所需的資訊。

以下範例示範如何使用 argNames 屬性

  • Java

  • Kotlin

@Before(
	value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", (1)
	argNames = "bean,auditable") (2)
public void audit(Object bean, Auditable auditable) {
	AuditCode code = auditable.value();
	// ... use code and bean
}
1 參考組合切入點運算式中定義的 publicMethod 具名切入點。
2 宣告 beanauditable 作為引數名稱。
@Before(
	value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", (1)
	argNames = "bean,auditable") (2)
fun audit(bean: Any, auditable: Auditable) {
	val code = auditable.value()
	// ... use code and bean
}
1 參考組合切入點運算式中定義的 publicMethod 具名切入點。
2 宣告 beanauditable 作為引數名稱。

如果第一個參數的型別為 JoinPointProceedingJoinPointJoinPoint.StaticPart,您可以從 argNames 屬性的值中省略參數的名稱。例如,如果您修改先前的通知以接收連接點物件,則 argNames 屬性不需要包含它

  • Java

  • Kotlin

@Before(
	value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", (1)
	argNames = "bean,auditable") (2)
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
	AuditCode code = auditable.value();
	// ... use code, bean, and jp
}
1 參考組合切入點運算式中定義的 publicMethod 具名切入點。
2 宣告 beanauditable 作為引數名稱。
@Before(
	value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", (1)
	argNames = "bean,auditable") (2)
fun audit(jp: JoinPoint, bean: Any, auditable: Auditable) {
	val code = auditable.value()
	// ... use code, bean, and jp
}
1 參考組合切入點運算式中定義的 publicMethod 具名切入點。
2 宣告 beanauditable 作為引數名稱。

對於不收集任何其他連接點內容的通知方法而言,給予 JoinPointProceedingJoinPointJoinPoint.StaticPart 型別的第一個參數特殊處理特別方便。在這種情況下,您可以省略 argNames 屬性。例如,以下通知不需要宣告 argNames 屬性

  • Java

  • Kotlin

@Before("com.xyz.Pointcuts.publicMethod()") (1)
public void audit(JoinPoint jp) {
	// ... use jp
}
1 參考組合切入點運算式中定義的 publicMethod 具名切入點。
@Before("com.xyz.Pointcuts.publicMethod()") (1)
fun audit(jp: JoinPoint) {
	// ... use jp
}
1 參考組合切入點運算式中定義的 publicMethod 具名切入點。

使用引數繼續

我們稍早提到,我們將描述如何編寫一個在 Spring AOP 和 AspectJ 中一致運作的帶有引數的 proceed 呼叫。解決方案是確保通知簽名依序繫結每個方法參數。以下範例示範如何執行此操作

  • Java

  • Kotlin

@Around("execution(List<Account> find*(..)) && " +
		"com.xyz.CommonPointcuts.inDataAccessLayer() && " +
		"args(accountHolderNamePattern)") (1)
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
		String accountHolderNamePattern) throws Throwable {
	String newPattern = preProcess(accountHolderNamePattern);
	return pjp.proceed(new Object[] {newPattern});
}
1 參考 共用具名切入點定義 中定義的具名切入點 inDataAccessLayer
@Around("execution(List<Account> find*(..)) && " +
		"com.xyz.CommonPointcuts.inDataAccessLayer() && " +
		"args(accountHolderNamePattern)") (1)
fun preProcessQueryPattern(pjp: ProceedingJoinPoint,
						accountHolderNamePattern: String): Any? {
	val newPattern = preProcess(accountHolderNamePattern)
	return pjp.proceed(arrayOf<Any>(newPattern))
}
1 參考 共用具名切入點定義 中定義的具名切入點 inDataAccessLayer

在許多情況下,您無論如何都會執行此繫結(如先前的範例所示)。

通知排序

當多個通知片段都想在同一個連接點執行時會發生什麼事?Spring AOP 遵循與 AspectJ 相同的優先順序規則來判斷通知執行的順序。最高優先順序的通知「在進入時」優先執行(因此,如果給定兩個前置通知,則優先順序較高的通知先執行)。「在從連接點離開時」,最高優先順序的通知最後執行(因此,如果給定兩個後置通知,則優先順序較高的通知將第二個執行)。

當在不同切面中定義的兩個通知片段都需要在同一個連接點執行時,除非您另行指定,否則執行順序未定義。您可以透過指定優先順序來控制執行順序。這可以透過一般的 Spring 方式完成,方法是在切面類別中實作 org.springframework.core.Ordered 介面,或使用 @Order 注解對其進行註解。給定兩個切面,從 Ordered.getOrder()(或註解值)傳回較低值的切面具有較高的優先順序。

特定切面的每個不同通知型別在概念上都旨在直接應用於連接點。因此,@AfterThrowing 通知方法不應從隨附的 @After/@AfterReturning 方法接收例外。

在同一個 @Aspect 類別中定義且需要在同一個連接點執行的通知方法,會根據其通知型別指派優先順序,順序從最高到最低優先順序如下:@Around@Before@After@AfterReturning@AfterThrowing。但是請注意,@After 通知方法將在同一個切面中的任何 @AfterReturning@AfterThrowing 通知方法之後有效地被調用,遵循 AspectJ 對於 @After 的「after finally advice」語意。

當在同一個 @Aspect 類別中定義的兩個相同型別的通知片段(例如,兩個 @After 通知方法)都需要在同一個連接點執行時,排序是未定義的(因為沒有辦法透過反射檢索由 javac 編譯的類別的原始碼宣告順序)。考慮將此類通知方法摺疊為每個 @Aspect 類別中每個連接點一個通知方法,或將通知片段重構為您可以透過 Ordered@Order 在切面層級排序的分開的 @Aspect 類別。