宣告通知

通知與切點表示式關聯,並在匹配的切點方法執行之前、之後或周圍執行。切點表示式可以是內聯切點,也可以是對命名切點的引用。

前置通知

您可以透過使用 @Before 註解在切面中宣告前置通知。

下面示例使用內聯切點表示式。

  • 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() {
		// ...
	}
}

後置返回通知

後置返回通知在匹配的方法執行正常返回後執行。您可以使用 @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() {
		// ...
	}
}
您可以在同一個切面中擁有多個通知宣告(以及其他成員)。我們在這些示例中只展示單個通知宣告,以突出各自的效果。

有時,您需要在通知體中訪問實際返回的值。您可以使用繫結返回值形式的 @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,匹配任何返回值)的方法執行。

請注意,使用後置返回通知時,不可能返回一個完全不同的引用。

後置異常通知

後置異常通知在匹配的方法執行因丟擲異常而退出時執行。您可以使用 @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 方法。

後置(最終)通知

後置(最終)通知在匹配的方法執行退出時執行。它透過使用 @After 註解宣告。後置通知必須準備好處理正常和異常返回情況。它通常用於釋放資源和類似目的。下面示例展示瞭如何使用後置最終通知

  • 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 通知被定義為“後置最終通知”,類似於 try-catch 語句中的 finally 塊。它會在任何結果下被呼叫,無論是正常返回還是從連線點(使用者宣告的目標方法)丟擲異常,這與 @AfterReturning 只應用於成功的正常返回不同。

環繞通知

最後一種通知型別是環繞通知。環繞通知在匹配方法的執行“周圍”執行。它有機會在方法執行之前和之後都執行工作,並決定方法何時、如何以及是否實際執行。如果您需要在方法執行之前和之後以執行緒安全的方式共享狀態(例如,啟動和停止計時器),通常會使用環繞通知。

始終使用滿足您需求的最低能力的通知形式。

例如,如果前置通知足以滿足您的需求,請不要使用環繞通知。

環繞通知透過使用 @Around 註解標註方法來宣告。該方法應將其返回型別宣告為 Object,並且該方法的第一個引數必須是 ProceedingJoinPoint 型別。在通知方法的體內,您必須在 ProceedingJoinPoint 上呼叫 proceed(),以便底層方法執行。不帶引數地呼叫 proceed() 將導致呼叫者的原始引數被傳遞給底層方法。對於高階用例,存在一個接受引數陣列 (Object[]) 的 proceed() 方法的過載變體。陣列中的值將在呼叫底層方法時用作引數。

使用 Object[] 呼叫 proceed 的行為與 AspectJ 編譯器編譯的環繞通知的 proceed 行為略有不同。對於使用傳統 AspectJ 語言編寫的環繞通知,傳遞給 proceed 的引數數量必須與傳遞給環繞通知的引數數量匹配(而不是底層連線點接受的引數數量),並且在給定引數位置傳遞給 proceed 的值將替換連線點處該值所繫結的實體的原始值(如果這暫時難以理解,請勿擔心)。

Spring 採用的方法更簡單,並且更匹配其基於代理、僅執行的語義。您只需要在透過 AspectJ 編譯器和織入器編譯為 Spring 編寫的 @AspectJ 切面並使用帶引數的 proceed 時瞭解此差異。有一種編寫此類切面的方法,可以在 Spring AOP 和 AspectJ 之間實現 100% 相容,這將在下一節關於通知引數中討論。

環繞通知返回的值是方法呼叫者看到的返回值。例如,一個簡單的快取切面如果快取中存在則可以返回快取中的值,如果不存在則呼叫 proceed()(並返回該值)。注意,可以在環繞通知體內呼叫 proceed 一次、多次或完全不呼叫。所有這些都是合法的。

如果您將環繞通知方法的返回型別宣告為 void,將始終向呼叫者返回 null,實際上忽略了對 proceed() 的任何呼叫的結果。因此建議環繞通知方法宣告 Object 作為返回型別。通知方法通常應返回呼叫 proceed() 返回的值,即使底層方法的返回型別是 void。然而,根據用例,通知可以選擇返回快取值、包裝值或某些其他值。

下面示例展示瞭如何使用環繞通知

  • 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
	}
}

通知引數

Spring 提供完全型別化的通知,這意味著您在通知簽名中宣告所需的引數(如我們在前面的返回和丟擲示例中所見),而不是始終使用 Object[] 陣列。我們將在本節後面介紹如何使引數和其他上下文值在通知體中可用。首先,我們來看看如何編寫通用通知,它可以找出通知當前正在通知的方法。

訪問當前 JoinPoint

任何通知方法都可以將其第一個引數宣告為 org.aspectj.lang.JoinPoint 型別。注意,環繞通知必須將其第一個引數宣告為 ProceedingJoinPoint 型別,它是 JoinPoint 的子類。

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

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

  • getThis():返回代理物件。

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

  • getSignature():返回被通知方法的描述。

  • toString():列印被通知方法的有用描述。

更多詳細資訊請參閱javadoc

向通知傳遞引數

我們已經瞭解瞭如何繫結返回值或異常值(使用後置返回和後置異常通知)。為了使引數值在通知體中可用,您可以使用 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 程式設計指南。

代理物件 (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 命名切點。

通知引數和泛型

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<?> 並手動檢查元素的型別。

確定引數名稱

通知呼叫中的引數繫結依賴於將切點表示式中使用的名稱與通知和切點方法簽名中宣告的引數名稱進行匹配。

本節交替使用術語 argumentparameter,因為 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 宣告為引數名稱。

對型別為 JoinPoint, ProceedingJoinPointJoinPoint.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 命名切點。

帶引數的執行

我們之前提到過,我們將描述如何編寫一個帶引數的 proceed 呼叫,使其在 Spring AOP 和 AspectJ 中保持一致。解決方案是確保通知簽名按順序繫結每個方法引數。以下示例展示瞭如何實現這一點

  • 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 相同的優先順序規則來確定通知的執行順序。優先順序最高的通知在“進入”時先執行(因此,給定兩個前置通知,優先順序最高的那個先執行)。從連線點“退出”時,優先順序最高的通知最後執行(因此,給定兩個後置通知,優先順序最高的那個將第二個執行)。

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

特定切面的每種不同通知型別在概念上都應直接應用於連線點。因此,@AfterThrowing 通知方法不應接收來自伴隨的 @After/@AfterReturning 方法丟擲的異常。

在同一個 @Aspect 類中定義、需要在同一個連線點執行的通知方法根據其通知型別分配優先順序,順序從高到低依次為:@Around, @Before, @After, @AfterReturning, @AfterThrowing。但請注意,根據 AspectJ 對 @After 的“after finally advice”語義,同一個切面中的 @After 通知方法將在任何 @AfterReturning@AfterThrowing 通知方法之後有效呼叫。

當定義在同一個 @Aspect 類中的兩個相同型別的通知(例如,兩個 @After 通知方法)都需要在同一個連線點執行時,其順序是未定義的(因為對於 javac 編譯的類,無法透過反射獲取原始碼宣告順序)。考慮將此類通知方法合併到每個 @Aspect 類中每個連線點的一個通知方法中,或者將這些通知重構到獨立的 @Aspect 類中,您可以透過 Ordered@Order 在切面級別進行排序。