宣告通知

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

前置通知

您可以透過使用@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()將導致在呼叫底層方法時向其提供呼叫者的原始引數。對於高階用例,proceed()方法有一個過載變體,它接受一個引數陣列(Object[])。陣列中的值將用作呼叫底層方法時的引數。

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

Spring 採取的方法更簡單,並且更適合其基於代理的、僅執行的語義。如果您編譯為 Spring 編寫的@AspectJ切面並使用帶有 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 AOP 可以處理類宣告和方法引數中使用的泛型。假設您有以下泛型型別:

訪問當前JoinPoint

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

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

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

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

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

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

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

有關更多詳細資訊,請參見javadoc

向通知傳遞引數

我們已經瞭解瞭如何繫結返回值或異常值(使用後置返回和後置丟擲通知)。要使引數值可用於通知主體,您可以使用args的繫結形式。如果您在args表示式中使用引數名代替型別名,則當呼叫通知時,相應引數的值將作為引數值傳遞。一個示例應該能更清楚地說明這一點。假設您希望通知 DAO 操作的執行,該操作將Account物件作為第一個引數,並且您需要訪問通知主體中的帳戶。您可以編寫以下內容:

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

確定引數名稱

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

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

當在不同切面中定義的兩個通知都需要在同一個連線點執行時,除非您另行指定,否則執行順序是不確定的。您可以透過指定優先順序來控制執行順序。這可以透過在切面類中實現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類中每個連線點的一個通知方法,或者將通知分解為單獨的@Aspect類,然後您可以透過Ordered@Order在切面級別對它們進行排序。

© . This site is unofficial and not affiliated with VMware.