宣告切入點
切入點決定了感興趣的連線點,從而使我們能夠控制通知何時執行。Spring AOP 只支援 Spring bean 的方法執行連線點,因此您可以將切入點視為匹配 Spring bean 上方法的執行。切入點宣告由兩部分組成:包含名稱和任何引數的簽名,以及準確確定我們感興趣的方法執行的切入點表示式。在 @AspectJ 註解風格的 AOP 中,切入點簽名由常規方法定義提供,切入點表示式透過使用 @Pointcut 註解指示(作為切入點簽名的方法必須具有 void 返回型別)。
一個例子可能有助於明確切入點簽名和切入點表示式之間的區別。以下示例定義了一個名為 anyOldTransfer 的切入點,它匹配任何名為 transfer 的方法的執行。
-
Java
-
Kotlin
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
@Pointcut("execution(* transfer(..))") // the pointcut expression
private fun anyOldTransfer() {} // the pointcut signature
形成 @Pointcut 註解值的切入點表示式是一個常規的 AspectJ 切入點表示式。有關 AspectJ 切入點語言的完整討論,請參閱 AspectJ 程式設計指南(以及擴充套件,AspectJ 5 開發人員手冊)或關於 AspectJ 的書籍(例如 Colyer 等人的 Eclipse AspectJ,或 Ramnivas Laddad 的 AspectJ in Action)。
支援的切入點指示符
Spring AOP 支援以下 AspectJ 切入點指示符(PCD)用於切入點表示式:
-
execution: 用於匹配方法執行連線點。這是在使用 Spring AOP 時要使用的主要切入點指示符。 -
within: 將匹配限制在特定型別內的連線點(使用 Spring AOP 時,在匹配型別中宣告的方法的執行)。 -
this: 將匹配限制在連線點(使用 Spring AOP 時,方法的執行),其中 bean 引用(Spring AOP 代理)是給定型別的例項。 -
target: 將匹配限制在連線點(使用 Spring AOP 時,方法的執行),其中目標物件(被代理的應用程式物件)是給定型別的例項。 -
args: 將匹配限制在連線點(使用 Spring AOP 時,方法的執行),其中引數是給定型別的例項。 -
@target: 將匹配限制在連線點(使用 Spring AOP 時,執行物件的類具有給定型別的註解)。 -
@args: 將匹配限制在連線點(使用 Spring AOP 時,實際傳遞的引數的執行時型別具有給定型別的註解)。 -
@within: 將匹配限制在具有給定註解的型別內的連線點(使用 Spring AOP 時,在具有給定註解的型別中宣告的方法的執行)。 -
@annotation: 將匹配限制在連線點的主體(在 Spring AOP 中執行的方法)具有給定註解的情況。
由於 Spring AOP 將匹配限制為僅方法執行連線點,因此前面關於切入點指示符的討論給出了比 AspectJ 程式設計指南中更窄的定義。此外,AspectJ 本身具有基於型別的語義,並且在執行連線點,this 和 target 都引用同一個物件:執行方法的物件。Spring AOP 是一個基於代理的系統,它區分代理物件本身(繫結到 this)和代理後面的目標物件(繫結到 target)。
|
由於 Spring AOP 框架基於代理的性質,目標物件內的呼叫,根據定義,不會被攔截。對於 JDK 代理,只能攔截代理上的公共介面方法呼叫。對於 CGLIB,可以攔截代理上的公共和受保護方法呼叫(如果需要,甚至可以攔截包可見方法)。但是,透過代理進行的常見互動應始終透過公共簽名進行設計。 請注意,切入點定義通常與任何被攔截的方法匹配。如果切入點嚴格只用於公共方法,即使在 CGLIB 代理場景中可能存在非公共互動,也需要相應地進行定義。 如果您的攔截需求包括目標類中的方法呼叫甚至建構函式,請考慮使用 Spring 驅動的 原生 AspectJ 織入,而不是 Spring 基於代理的 AOP 框架。這構成了一種不同模式的 AOP 使用,具有不同的特性,因此在做出決定之前請務必熟悉織入。 |
Spring AOP 還支援一個名為 bean 的附加 PCD。此 PCD 允許您將連線點的匹配限制為特定的命名 Spring bean 或一組命名 Spring bean(當使用萬用字元時)。bean PCD 具有以下形式:
bean(idOrNameOfBean)
idOrNameOfBean 令牌可以是任何 Spring bean 的名稱。提供了使用 * 字元的有限萬用字元支援,因此,如果您為 Spring bean 建立一些命名約定,您可以編寫一個 bean PCD 表示式來選擇它們。與其他切入點指示符一樣,bean PCD 也可以與 &&(與)、||(或)和 !(非)運算子一起使用。
|
|
組合切入點表示式
您可以使用 &&、|| 和 ! 組合切入點表示式。您還可以按名稱引用切入點表示式。以下示例顯示了三個切入點表示式:
-
Java
-
Kotlin
package com.xyz;
public class Pointcuts {
@Pointcut("execution(public * *(..))")
public void publicMethod() {} (1)
@Pointcut("within(com.xyz.trading..*)")
public void inTrading() {} (2)
@Pointcut("publicMethod() && inTrading()")
public void tradingOperation() {} (3)
}
| 1 | 如果方法執行連線點代表任何公共方法的執行,則 publicMethod 匹配。 |
| 2 | 如果方法執行在交易模組中,則 inTrading 匹配。 |
| 3 | 如果方法執行代表交易模組中的任何公共方法,則 tradingOperation 匹配。 |
package com.xyz
class Pointcuts {
@Pointcut("execution(public * *(..))")
fun publicMethod() {} (1)
@Pointcut("within(com.xyz.trading..*)")
fun inTrading() {} (2)
@Pointcut("publicMethod() && inTrading()")
fun tradingOperation() {} (3)
}
| 1 | 如果方法執行連線點代表任何公共方法的執行,則 publicMethod 匹配。 |
| 2 | 如果方法執行在交易模組中,則 inTrading 匹配。 |
| 3 | 如果方法執行代表交易模組中的任何公共方法,則 tradingOperation 匹配。 |
最佳實踐是如上所示,從較小的命名切入點構建更復雜的切入點表示式。按名稱引用切入點時,適用正常的 Java 可見性規則(您可以在同一型別中看到 private 切入點,在層次結構中看到 protected 切入點,在任何地方看到 public 切入點,等等)。可見性不影響切入點匹配。
共享命名切入點定義
在處理企業應用程式時,開發人員經常需要從多個切面中引用應用程式的模組和特定的操作集。為此,我們建議定義一個專用類,用於捕獲常用的命名切入點表示式。此類通常類似於以下 CommonPointcuts 示例(儘管類的命名由您決定):
-
Java
-
Kotlin
package com.xyz;
import org.aspectj.lang.annotation.Pointcut;
public class CommonPointcuts {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.service..*)")
public void inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.dao..*)")
public void inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.abc.service and com.xyz.def.service) then
* the pointcut expression "execution(* com.xyz..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz..service.*.*(..))")
public void businessService() {}
/**
* A data access operation is the execution of any method defined on a
* DAO interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.dao.*.*(..))")
public void dataAccessOperation() {}
}
package com.xyz
import org.aspectj.lang.annotation.Pointcut
class CommonPointcuts {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.web..*)")
fun inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.service..*)")
fun inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.dao..*)")
fun inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.abc.service and com.xyz.def.service) then
* the pointcut expression "execution(* com.xyz..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz..service.*.*(..))")
fun businessService() {}
/**
* A data access operation is the execution of any method defined on a
* DAO interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.dao.*.*(..))")
fun dataAccessOperation() {}
}
您可以在任何需要切入點表示式的地方引用此類中定義的切入點,方法是引用類的完全限定名與 @Pointcut 方法的名稱相結合。例如,要使服務層具有事務性,您可以編寫以下內容,它引用了 com.xyz.CommonPointcuts.businessService() 命名切入點:
<aop:config>
<aop:advisor
pointcut="com.xyz.CommonPointcuts.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config> 和 <aop:advisor> 元素在 基於 Schema 的 AOP 支援 中討論。事務元素在 事務管理 中討論。
示例
Spring AOP 使用者最有可能經常使用 execution 切入點指示符。執行表示式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
除了返回型別模式(上述片段中的 ret-type-pattern)、名稱模式和引數模式之外,所有部分都是可選的。返回型別模式決定了方法的返回型別必須是什麼才能匹配連線點。* 最常被用作返回型別模式。它匹配任何返回型別。完全限定的型別名稱僅在方法返回給定型別時匹配。名稱模式匹配方法名。您可以使用 * 萬用字元作為名稱模式的全部或一部分。如果指定宣告型別模式,請包含一個尾隨的 . 以將其與名稱模式元件連線。引數模式稍微複雜一些:() 匹配不帶引數的方法,而 (..) 匹配任意數量(零個或多個)的引數。(*) 模式匹配帶有一個任何型別引數的方法。(*,String) 匹配帶兩個引數的方法。第一個可以是任何型別,而第二個必須是 String。有關更多資訊,請查閱 AspectJ 程式設計指南的 語言語義 部分。
以下示例顯示了一些常見的切入點表示式:
-
任何公共方法的執行
execution(public * *(..))
-
任何名稱以
set開頭的方法的執行execution(* set*(..))
-
AccountService介面定義的任何方法的執行execution(* com.xyz.service.AccountService.*(..))
-
service包中定義的任何方法的執行execution(* com.xyz.service.*.*(..))
-
服務包或其子包中定義的任何方法的執行
execution(* com.xyz.service..*.*(..))
-
服務包內的任何連線點(在 Spring AOP 中僅限於方法執行)
within(com.xyz.service.*)
-
服務包或其子包內的任何連線點(在 Spring AOP 中僅限於方法執行)
within(com.xyz.service..*)
-
代理實現
AccountService介面的任何連線點(在 Spring AOP 中僅限於方法執行)this(com.xyz.service.AccountService)
this更常用於繫結形式。有關如何在通知正文中使代理物件可用的資訊,請參閱 宣告通知 部分。 -
目標物件實現
AccountService介面的任何連線點(在 Spring AOP 中僅限於方法執行)target(com.xyz.service.AccountService)
target更常用於繫結形式。有關如何在通知正文中使目標物件可用的資訊,請參閱 宣告通知 部分。 -
接受單個引數且執行時傳遞的引數是
Serializable的任何連線點(在 Spring AOP 中僅限於方法執行)args(java.io.Serializable)
args更常用於繫結形式。有關如何在通知正文中使方法引數可用的資訊,請參閱 宣告通知 部分。請注意,此示例中給出的切入點與
execution(* *(java.io.Serializable))不同。args 版本匹配執行時傳遞的引數是Serializable,而 execution 版本匹配方法簽名宣告單個Serializable型別的引數。 -
目標物件具有
@Transactional註解的任何連線點(在 Spring AOP 中僅限於方法執行)@target(org.springframework.transaction.annotation.Transactional)
您也可以以繫結形式使用 @target。有關如何在通知正文中使註解物件可用的資訊,請參閱 宣告通知 部分。 -
目標物件的宣告型別具有
@Transactional註解的任何連線點(在 Spring AOP 中僅限於方法執行)@within(org.springframework.transaction.annotation.Transactional)
您也可以以繫結形式使用 @within。有關如何在通知正文中使註解物件可用的資訊,請參閱 宣告通知 部分。 -
執行方法具有
@Transactional註解的任何連線點(在 Spring AOP 中僅限於方法執行)@annotation(org.springframework.transaction.annotation.Transactional)
您也可以以繫結形式使用 @annotation。有關如何在通知正文中使註解物件可用的資訊,請參閱 宣告通知 部分。 -
接受單個引數且傳遞的引數的執行時型別具有
@Classified註解的任何連線點(在 Spring AOP 中僅限於方法執行)@args(com.xyz.security.Classified)
您也可以以繫結形式使用 @args。有關如何在通知正文中使註解物件可用的資訊,請參閱 宣告通知 部分。 -
在名為
tradeService的 Spring bean 上的任何連線點(在 Spring AOP 中僅限於方法執行)bean(tradeService)
-
在名稱匹配萬用字元表示式
*Service的 Spring bean 上的任何連線點(在 Spring AOP 中僅限於方法執行)bean(*Service)
編寫良好的切入點
在編譯期間,AspectJ 會處理切入點以最佳化匹配效能。檢查程式碼並確定每個連線點是否匹配(靜態或動態)給定切入點是一個耗時的過程。(動態匹配意味著匹配無法完全透過靜態分析確定,並且在程式碼中放置了一個測試以確定程式碼執行時是否存在實際匹配)。在首次遇到切入點宣告時,AspectJ 會將其重寫為匹配過程的最佳形式。這意味著什麼?基本上,切入點被重寫為 DNF(析取正規化),並且切入點的元件經過排序,以便首先檢查那些評估成本較低的元件。這意味著您不必擔心理解各種切入點指示符的效能,並且可以在切入點宣告中以任何順序提供它們。
然而,AspectJ 只能根據被告知的資訊工作。為了獲得最佳的匹配效能,您應該考慮您想要實現的目標,並在定義中儘可能縮小匹配的搜尋空間。現有的指示符自然分為三類:型別、作用域和上下文。
-
型別指示符選擇特定型別的連線點:
execution、get、set、call和handler。 -
作用域指示符選擇一組感興趣的連線點(可能屬於多種型別):
within和withincode。 -
上下文指示符根據上下文進行匹配(並可選地繫結):
this、target和@annotation。
一個編寫良好的切入點應至少包含前兩種型別(型別和作用域)。您可以包含上下文指示符以根據連線點上下文進行匹配或繫結該上下文以在通知中使用。僅提供型別指示符或僅提供上下文指示符也可以工作,但由於額外的處理和分析,可能會影響織入效能(時間和記憶體使用)。作用域指示符匹配速度非常快,使用它們意味著 AspectJ 可以非常快速地排除不應進一步處理的連線點組。一個好的切入點如果可能的話應該始終包含一個。