宣告切入點

切入點決定了感興趣的連接點,因此使我們能夠控制建議何時運行。 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 中運行的方法)具有給定註解。

其他切入點類型

完整的 AspectJ 切入點語言支援其他切入點指示符,這些指示符在 Spring 中不受支援:callgetsetpreinitializationstaticinitializationinitializationhandleradviceexecutionwithincodecflowcflowbelowif@this@withincode。在 Spring AOP 解釋的切入點運算式中使用這些切入點指示符會導致拋出 IllegalArgumentException

Spring AOP 支援的切入點指示符集可能會在未來的版本中擴充,以支援更多的 AspectJ 切入點指示符。

由於 Spring AOP 將匹配限制為僅方法執行連接點,因此前面關於切入點指示符的討論給出了比您在 AspectJ 程式設計指南中找到的更狹窄的定義。此外,AspectJ 本身具有基於類型的語意,並且在執行連接點,thistarget 都引用同一個物件:執行該方法的物件。 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 Token 可以是任何 Spring Bean 的名稱。提供了使用 * 字元的有限萬用字元支援,因此,如果您為您的 Spring Bean 建立了一些命名約定,您可以編寫一個 bean PCD 運算式來選擇它們。與其他切入點指示符一樣,bean PCD 也可以與 && (and)、|| (or) 和 ! (negation) 運算子一起使用。

bean PCD 僅在 Spring AOP 中受支援,而在原生 AspectJ 編織中不受支援。它是 AspectJ 定義的標準 PCD 的 Spring 特定擴充功能,因此不適用於在 @Aspect 模型中宣告的切面。

bean PCD 在實例層級(基於 Spring Bean 名稱概念)而不是僅在類型層級(編織式 AOP 僅限於此層級)上運行。基於實例的切入點指示符是 Spring 基於代理的 AOP 框架及其與 Spring Bean 工廠緊密整合的特殊功能,在 Spring Bean 工廠中,通過名稱識別特定 Bean 是自然而直接的。

組合切入點運算式

您可以使用 &&, ||! 來組合切入點運算式。您也可以按名稱引用切入點運算式。以下範例顯示了三個切入點運算式

  • 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 運算式的格式如下

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.*.*(..))
  • 在 service 套件或其子套件之一中定義的任何方法的執行

    execution(* com.xyz.service..*.*(..))
  • service 套件中的任何連接點(僅在 Spring AOP 中為方法執行)

    within(com.xyz.service.*)
  • 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。請參閱宣告 Advice 章節,瞭解如何在 Advice 主體中使用註解物件。
  • 任何連接點(在 Spring AOP 中僅限方法執行),其中執行的方法具有 @Transactional 註解

    @annotation(org.springframework.transaction.annotation.Transactional)
    您也可以在綁定形式中使用 @annotation。請參閱宣告 Advice 章節,瞭解如何在 Advice 主體中使用註解物件。
  • 任何連接點(在 Spring AOP 中僅限方法執行),它接受單一參數,且傳遞的引數的執行時期型別具有 @Classified 註解

    @args(com.xyz.security.Classified)
    您也可以在綁定形式中使用 @args。請參閱宣告 Advice 章節,瞭解如何在 Advice 主體中使用註解物件。
  • 任何在名為 tradeService 的 Spring Bean 上的連接點(在 Spring AOP 中僅限方法執行)

    bean(tradeService)
  • 任何在名稱符合萬用字元運算式 *Service 的 Spring Bean 上的連接點(在 Spring AOP 中僅限方法執行)

    bean(*Service)

撰寫良好的 Pointcut

在編譯期間,AspectJ 會依序處理 Pointcut,以最佳化比對效能。檢查程式碼並判斷每個連接點是否符合給定的 Pointcut(靜態或動態)是一個成本很高的過程。(動態比對表示比對無法完全從靜態分析判斷,並且在程式碼中放置一個測試,以判斷程式碼執行時是否存在實際比對)。在第一次遇到 Pointcut 宣告時,AspectJ 會將其重寫為最佳化形式以進行比對過程。這代表什麼?基本上,Pointcut 會以 DNF(析取範式)重寫,並且 Pointcut 的組件會被排序,以便首先檢查那些評估成本較低的組件。這表示您不必擔心理解各種 Pointcut 設計器的效能,並且可以在 Pointcut 宣告中以任何順序提供它們。

然而,AspectJ 只能使用它被告知的內容。為了獲得最佳的比對效能,您應該考慮您嘗試達成的目標,並在定義中盡可能縮小比對的搜尋空間。現有的設計器自然分為三組:種類、範圍和上下文

  • 種類設計器選擇特定種類的連接點:executiongetsetcallhandler

  • 範圍設計器選擇一組感興趣的連接點(可能有很多種類):withinwithincode

  • 上下文設計器根據上下文比對(並可選擇性地綁定):thistarget@annotation

一個寫得好的 Pointcut 應至少包含前兩種型別(種類和範圍)。您可以包含上下文設計器,以根據連接點上下文進行比對,或綁定該上下文以在 Advice 中使用。僅提供種類設計器或僅提供上下文設計器可以運作,但可能會影響編織效能(使用的時間和記憶體),因為需要額外的處理和分析。範圍設計器比對速度非常快,使用它們表示 AspectJ 可以非常快速地排除不應進一步處理的連接點組。如果可能,一個好的 Pointcut 應始終包含一個範圍設計器。