宣告通知
通知與切入點表示式相關聯,在切入點匹配的方法執行之前、之後或周圍執行。切入點表示式可以是內聯切入點,也可以是命名切入點的引用。
前置通知
您可以透過使用@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)的方法執行。
|
請注意, |
後置(最終)通知
後置(最終)通知在匹配的方法執行退出時執行。它透過使用@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 中的 |
環繞通知
最後一種通知是環繞通知。環繞通知在匹配方法的執行“周圍”執行。它有機會在方法執行之前和之後執行工作,並決定方法何時、如何以及是否實際執行。如果您需要在方法執行之前和之後以執行緒安全的方式共享狀態(例如,啟動和停止計時器),通常會使用環繞通知。
|
始終使用滿足您需求的最低功能形式的通知。 例如,如果前置通知足以滿足您的需求,請不要使用環繞通知。 |
環繞通知透過使用@Around註解方法來宣告。該方法應將其返回型別宣告為Object,並且該方法的第一個引數必須是ProceedingJoinPoint型別。在通知方法的主體中,您必須在ProceedingJoinPoint上呼叫proceed(),以便底層方法執行。不帶引數呼叫proceed()將導致在呼叫底層方法時向其提供呼叫者的原始引數。對於高階用例,proceed()方法有一個過載變體,它接受一個引數陣列(Object[])。陣列中的值將用作呼叫底層方法時的引數。
|
當使用 Spring 採取的方法更簡單,並且更適合其基於代理的、僅執行的語義。如果您編譯為 Spring 編寫的 |
環繞通知返回的值是方法呼叫者看到的值。例如,一個簡單的快取切面可以從快取中返回一個值(如果它有),或者在沒有快取時呼叫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.ParameterAPI 確定引數名稱。要求使用javac的-parameters標誌編譯程式碼。Java 8+ 上的推薦方法。 AspectJAdviceParameterNameDiscoverer-
從切入點表示式、
returning和throwing子句推斷引數名稱。有關使用的演算法的詳細資訊,請參見javadoc。
顯式引數名稱
@AspectJ 通知和切入點註解有一個可選的argNames屬性,您可以使用它來指定註解方法的引數名稱。
|
如果 @AspectJ 切面已由 AspectJ 編譯器( 同樣,如果 @AspectJ 切面已使用 |
以下示例演示瞭如何使用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 | 宣告bean和auditable為引數名稱。 |
@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 | 宣告bean和auditable為引數名稱。 |
如果第一個引數的型別是JoinPoint、ProceedingJoinPoint或JoinPoint.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 | 宣告bean和auditable為引數名稱。 |
@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 | 宣告bean和auditable為引數名稱。 |
對型別為JoinPoint、ProceedingJoinPoint或JoinPoint.StaticPart的第一個引數的特殊處理對於不收集任何其他連線點上下文的通知方法特別方便。在這種情況下,您可以省略argNames屬性。例如,以下通知不需要宣告argNames屬性:
帶引數的執行
我們之前提到過,我們將描述如何編寫一個在 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()返回較低值(或註解值)的切面具有較高的優先順序。
|
特定切面的每種不同通知型別概念上都旨在直接應用於連線點。因此, 在同一個 當在同一個 |