宣告切入點

切入點確定感興趣的連線點,從而使我們能夠控制通知何時執行。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 Developer’s Notebook)或 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 不支援的額外切入點指示符:call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @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 還支援一個額外的 PCD,名為 bean。此 PCD 允許你將連線點的匹配限制為特定的命名 Spring bean 或一組命名的 Spring bean(使用萬用字元時)。bean PCD 具有以下形式

bean(idOrNameOfBean)

idOrNameOfBean 標記可以是任何 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 工廠緊密整合的一個特殊能力,在這種情況下,按名稱識別特定 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 的繫結形式。請參閱 宣告通知 部分,瞭解如何在通知體中使註解物件可用。
  • 正在執行的方法具有 @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 只能處理它被告知的內容。為了獲得最佳的匹配效能,您應該考慮您想要實現什麼,並在定義中儘可能地縮小匹配的搜尋空間。現有的指示符自然分為三組:種類指示符(kinded)、範圍指示符(scoping)和上下文指示符(contextual)。

  • 種類指示符選擇特定種類的連線點:executiongetsetcallhandler

  • 範圍指示符選擇一組感興趣的連線點(可能包含多種種類):withinwithincode

  • 上下文指示符根據上下文進行匹配(並可選地繫結):thistarget@annotation

一個編寫良好的切入點至少應包含前兩種型別(種類指示符和範圍指示符)。您可以包含上下文指示符以根據連線點上下文進行匹配或繫結該上下文供通知使用。只提供種類指示符或只提供上下文指示符也可以,但可能會影響織入效能(使用的時間和記憶體),因為需要額外的處理和分析。範圍指示符的匹配速度非常快,使用它們意味著 AspectJ 可以非常快速地排除不應進一步處理的連線點組。如果可能的話,一個好的切入點應始終包含一個範圍指示符。