面向切面的編程(AOP)通過提供另一種思考程序結構的方式來補充面向對像的編程(OOP)。OOP中模塊化的關鍵單元是類,而在AOP中模塊化是切面。切面使關注點(例如事務管理)的模塊化可以跨越多種類型和對象。(這種關注在AOP文獻中通常被稱為“跨領域”關注。)
Spring的關鍵組件之一是AOP框架。雖然Spring IoC容器不依賴於AOP(這意味著如果你不想使用AOP,就不需要使用AOP),但AOP對Spring IoC進行了補充,提供了一個非常強大的中間件解決方案。
具有AspectJ切入點的Spring AOP
Spring提供了使用基於schema的方法或@AspectJ註解樣式來編寫自定義切面的簡單而強大的方法。這兩種樣式都提供了完全類型化的建議,並使用了AspectJ切入點語言,同時仍然使用Spring AOP進行編織。
本章討論基於
schema
和基於@AspectJ
的AOP支持。下一章將討論較低級別的AOP支持。
AOP在Spring框架中用於:
- 提供聲明式企業服務。此類服務中最重要的是聲明式事務管理。
- 讓用戶實現自定義切面,並用AOP補充其對OOP的使用。
如果你只對通用聲明性服務或其他預包裝的聲明性中間件服務(例如池)感興趣,則無需直接使用Spring AOP,並且可以跳過本章的大部分內容。
5.1 AOP概念
讓我們首先定義一些主要的AOP概念和術語。這些術語不是特定於Spring的。不幸的是,AOP術語並不是特別直觀。但是,如果使用Spring自己的術語,將會更加令人困惑。
- 切面:涉及多個類別的關注點的模塊化。事務管理是企業Java應用程序中橫切關注的一個很好的例子。在Spring AOP中,切面是通過使用常規類(基於schema的方法)或使用
@Aspect
註解(@AspectJ樣式)註釋的常規類來實現的。 - 連接點:程序執行過程中的一點,例如方法的執行或異常的處理。在Spring AOP中,連接點始終代表方法的執行。
- 通知:切面在特定的連接點處採取的操作。不同類型的通知包括:“
around
”,“before
”和“after
”通知。(通知類型將在後面討論。)包括Spring在內的許多AOP框架都將通知建模為攔截器,並在連接點周圍維護一系列攔截器。 - 切入點:表示匹配連接點。通知與切入點表達式關聯,並在與該切入點匹配的任何連接點處運行(例如,執行具有特定名稱的方法)。切入點表達式匹配的連接點的概念是AOP的核心,默認情況下,Spring使用
AspectJ
切入點表達語言。 - 引入:在類型上聲明其他方法或字段。Spring AOP允許你向任何通知對象引入新的接口(和相應的實現)。例如,你可以使用引入使Bean實現IsModified接口,以簡化緩存。(引入在
AspectJ
社區中稱為類型間聲明。) - 目標對象:一個或多個切面通知的對象。也稱為“通知對象”。由於Spring AOP是使用運行時代理實現的,因此該對象始終是代理對象。
- AOP代理:由AOP框架創建的對象,用於實現切面約定(通知方法執行等)。在Spring Framework中,AOP代理是
JDK
動態代理或CGLIB
代理。 - 編織:將切面與其他應用程序類型或對象鏈接以創建通知的對象。這可以在編譯時(例如,使用
AspectJ
編譯器),加載時或在運行時完成。像其他純Java AOP框架一樣,Spring AOP在運行時執行編織。
Spring AOP包括以下類型的通知:
- 前置通知:在連接點之前運行但無法阻止執行流前進到連接點的通知(除非它引發異常)。
- 後置通知:連接點正常完成後要運行的通知(例如,如果某個方法返回而沒有引發異常)。
- 後置異常通知:如果方法因拋出異常而退出,將執行的通知。
- 最終通知:無論連接點退出的方式如何(正常或異常返回),都將執行通知。
- 環繞通知:圍繞連接點的通知,例如方法調用。這是最強大的通知。環繞通知可以在方法調用之前和之後執行自定義行為。它還負責選擇是繼續到連接點,還是通過返回自己的返回值或拋出異常來簡化通知的方法執行。
環繞通知是最通用的通知。由於Spring AOP與AspectJ
一樣,提供了各種通知類型,因此我們建議你使用功能最弱的建議類型,以實現所需的行為。例如,如果你只需要使用方法的返回值更新緩存,則最好使用後置通知而不是環繞通知,儘管環繞通知可以完成相同的事情。使用最具體的通知類型可提供更簡單的編程模型,並減少出錯的可能性。例如,你不需要在用於環繞通知的JoinPoint
上調用proceed()
方法,因此,你不會失敗。
所有通知參數都是靜態類型的,因此你可以使用適當類型(例如,從方法執行返回的值的類型)而不是對象數組的 通知參數。
切入點匹配的連接點的概念是AOP的關鍵,它與僅提供攔截功能的舊技術不同。切入點使通知的目標獨立於面向對象的層次結構。例如,你可以將提供聲明性事務管理的環繞通知應用於跨越多個對象(例在服務層中的所有業務操作)的一組方法。
5.2 AOP能力和目標
Spring AOP是用純Java實現的。不需要特殊的編譯過程。Spring AOP不需要控制類加載器的層次結構,因此適合在Servlet容器或應用程序服務器中使用。
Spring AOP當前僅支持方法執行連接點(通知在Spring Bean上執行方法)。儘管可以在不破壞核心Spring AOP API的情況下添加對字段攔截的支持,但並未實現字段攔截。如果需要通知字段訪問和更新連接點,請考慮使用諸如AspectJ
之類的語言。
Spring AOP的AOP方法不同於大多數其他AOP框架。目的不是提供最完整的AOP實現(儘管Spring AOP相當強大)。相反,其目的是在AOP實現和Spring IoC之間提供緊密的集成,以幫助解決企業應用程序中的常見問題。
因此,例如,通常將Spring Framework的AOP功能與Spring IoC容器結合使用。通過使用常規bean定義語法來配置切面(儘管這允許強大的“自動代理”功能)。這是與其他AOP實現的關鍵區別。使用Spring AOP不能輕鬆或有效地完成一些事情,比如通知非常細粒度的對象(通常是域對象)。在這種情況下,AspectJ
是最佳選擇。但是,我們的經驗是,Spring AOP為AOP可以解決的企業Java應用程序中的大多數問題提供了出色的解決方案。
Spring AOP從未努力與AspectJ
競爭以提供全面的AOP解決方案。我們認為,基於代理的框架(如Spring AOP)和成熟的框架(如AspectJ
)都是有價值的,它們是互補的,而不是競爭。Spring無縫地將Spring AOP和IoC與AspectJ
集成在一起,以在基於Spring的一致應用程序架構中支持AOP的所有功能。這種集成不會影響Spring AOP API或AOP Alliance
API。Spring AOP仍然向後兼容。請參閱下一章,以討論Spring AOP API。
Spring框架的中心宗旨之一是非侵入性。這就是不應該強迫你將特定於框架的類和接口引入你的業務或領域模型的思想。但是,在某些地方,Spring Framework確實為你提供了將特定於Spring Framework的依賴項引入代碼庫的選項。提供此類選項的理由是,在某些情況下,以這種方式閱讀或編碼某些特定功能可能會變得更加容易。但是,Spring框架(幾乎)總是為你提供選擇:你可以自由地就哪個選項最適合你的特定用例或場景做出明智的決定。
與本章相關的一種選擇是選擇哪種AOP框架(以及哪種AOP樣式)。你可以選擇
AspectJ
和或Spring AOP。你也可以選擇@AspectJ
註解樣式方法或Spring XML配置樣式方法。本章選擇首先介紹@AspectJ
風格的方法,這不能表明Spring比Spring XML配置風格更喜歡@AspectJ
註釋風格的方法(備註:使用AspectJ
編寫例子不能說明Spring更喜歡AspectJ
註解編程)。有關每種樣式的“來龍去脈”的更完整討論,請參見選擇要使用的AOP聲明樣式。
5.3 AOP代理
Spring AOP默認將標準JDK動態代理用於AOP代理。這使得可以代理任何接口(或一組接口)。
Spring AOP也可以使用CGLIB代理。這對於代理類而不是接口是必需的。默認情況下,如果業務對象未實現接口,則使用CGLIB。由於對接口而不是對類進行編程是一種好習慣,因此業務類通常實現一個或多個業務接口。在某些情況下(可能極少發生),你需要通知在接口上未聲明的方法,或需要將代理對象作為具體類型傳遞給方法,則可以強制使用CGLIB。
掌握Spring AOP是基於代理的這一事實很重要。請參閱瞭解AOP代理以全面瞭解此實現細節的實際含義。
5.4 @AspectJ支持
@AspectJ
是一種將切面聲明為帶有註解的常規Java類的樣式。@AspectJ
樣式是AspectJ項目在AspectJ 5版本中引入的。Spring使用AspectJ
提供的用於切入點解析和匹配的庫來解釋與AspectJ 5相同的註解。但是,AOP運行時仍然是純Spring AOP,並且不依賴於AspectJ
編譯器或編織器。
使用
AspectJ
編譯器和編織器可以使用完整的AspectJ
語言,有關在Spring Applications中使用AspectJ進行了討論。
5.4.1 激活@AspectJ
支持
要在Spring配置中使用@AspectJ
切面,你需要啟用Spring支持以基於@AspectJ
切面配置Spring AOP,並根據這些切面是否通知對Bean進行自動代理。通過自動代理,我們的意思是,如果Spring確定一個或多個切面通知一個bean,它會自動為該bean生成一個代理來攔截方法調用並確保按需執行通知。
可以使用XML或Java樣式的配置來啟用@AspectJ
支持。無論哪種情況,你都需要確保AspectJ
的Aspectjweaver.jar
庫位於應用程序的類路徑(版本1.8或更高版本)上。該庫在AspectJ
發行版的lib
目錄中或從Maven Central存儲庫中獲取。
通過Java配置激活@AspectJ
通過Java @Configuration
啟用@AspectJ
支持,請添加@EnableAspectJAutoProxy
註解,如以下示例所示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
通過XML配置激活@AspectJ
通過基於XML的配置啟用@AspectJ
支持,請使用<aop:aspectj-autoproxy>
元素,如以下示例所示:
<aop:aspectj-autoproxy/>
假定你使用基於XML Schema的配置中所述的架構支持。有關如何在aop名稱空間中導入標籤的信息,請參見AOP schema。
5.4.2 聲明一個切面
啟用@AspectJ
支持後,Spring會自動檢測在應用程序上下文中使用@AspectJ
切面(有@Aspect
註解)的類定義的任何bean,並用於配置Spring AOP。接下來的兩個示例顯示了一個不太有用的切面所需的最小定義。
兩個示例中的第一個示例顯示了應用程序上下文中的常規bean定義,該定義指向具有@Aspect
註解的bean類:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
這兩個示例中的第二個示例顯示了NotVeryUsefulAspect
類定義,該類定義使用org.aspectj.lang.annotation.Aspect
註解進行註釋;
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
切面(使用@Aspect
註解的類)可以具有方法和字段,與任何其他類相同。它們還可以包含切入點、通知和引入(類型間)聲明。
通過組件掃描自動檢測切面
你可以將切面類註冊為Spring XML配置中的常規bean,也可以通過類路徑掃描自動檢測它們-與其他任何Spring管理的bean一樣。但是,請注意,
@Aspect
註解不足以在類路徑中進行自動檢測。為此,你需要添加一個單獨的@Component
註解(或者,按照Spring的組件掃描程序的規則,有條件的自定義構造型註解)。向其他切面提供通知?
在Spring AOP中,切面本身不能成為其他切面的通知目標。類上的
@Aspect
註解將其標記為一個切面,因此將其從自動代理中排除。
5.4.3 聲明切入點
切入點確定了感興趣的連接點,從而使我們能夠控制何時執行通知。Spring AOP僅支持Spring Bean的方法執行連接點,因此你可以將切入點視為與Spring Bean上的方法執行匹配。切入點聲明由兩部分組成:一個包含名稱和任何參數的簽名,以及一個切入點表達式,該表達式精確確定我們感興趣的方法執行。在AOP的@AspectJ
註解樣式中,常規方法定義提供了切入點簽名,並且使用@Pointcut
註解指示了切入點表達式(用作切入點簽名的方法必須具有void
返回類型)。一個示例可能有助於使切入點簽名和切入點表達式之間的區別變得清晰。下面的示例定義一個名為anyOldTransfer
的切入點,該切入點與任何名為transfer
方法的執行相匹配:
@Pointcut("execution(* transfer(..))") // 切入點表達式
private void anyOldTransfer() {} // 切入點方法簽名
形成@Pointcut
註解的值的切入點表達式是一個常規的AspectJ 5切入點表達式。有關AspectJ
的切入點語言的完整討論,請參見AspectJ編程指南(以及擴展,包括AspectJ 5開發人員手冊)或有關AspectJ的書籍之一(如《Eclipse AspectJ》或《 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不支持這些指示符)。在Spring AOP解釋的切入點表達式中使用這些切入點指示符會導致拋出IllegalArgumentException
。Spring AOP支持的切入點指示符集合可能會在將來的版本中擴展,以支持更多的
AspectJ
切入點指示符。
由於Spring AOP僅將匹配限制為僅方法執行連接點,因此前面對切入點指示符的討論所給出的定義比在AspectJ
編程指南中所能找到的要窄。此外,AspectJ
本身具有基於類型的語義,並且在執行連接點處,this
和target
都引用同一個對象:執行該方法的對象。Spring AOP是基於代理的系統,可區分代理對象本身(綁定到此對象)和代理背後的目標對象(綁定到目標)。
由於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可以與&&
(和)、||
(或)、和!
(否定)運算符一起使用。
Bean PCD僅在Spring AOP中受支持,而在本地
AspectJ
編織中不受支持。它是AspectJ
定義的標準PCD的特定於Spring的擴展,因此不適用於@Aspect
模型中聲明的切面。Bean PCD在實例級別(基於Spring bean名稱概念構建)上運行,而不是僅在類型級別(基於編織的AOP受其限制)上運行。基於實例的切入點指示符是Spring基於代理的AOP框架的特殊功能,並且與Spring bean工廠緊密集成,因此可以自然而直接地通過名稱識別特定bean。
組合切入點表達式
你可以使用&&
、||
和!
組合切入點表達式。你還可以按名稱引用切入點表達式。以下示例顯示了三個切入點表達式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} //1
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {} //2
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} //3
- 如果方法執行連接點表示任何公共方法的執行,則
anyPublicOperation
匹配。 - 如果交易模塊中有方法執行,則
inTrading
匹配。 - 如果方法執行代表交易模塊中的任何公共方法,則
tradingOperation
匹配。
最佳實踐是從較小的命名組件中構建更復雜的切入點表達式,如先前所示。按名稱引用切入點時,將應用常規的Java可見性規則(你可以看到相同類型的private切入點,層次結構中protected
的切入點,任何位置的public切入點,等等)。可見性不影響切入點匹配。
共享通用切入點定義
在企業級應用中,開發人員通常希望從多個方面引用應用程序的模塊和特定的操作集。我們建議為此定義一個 SystemArchitecture
切面,以捕獲常見的切入點表達式意圖。這樣的切面通常類似於以下示例:
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.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.someapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.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.someapp.abc.service and com.xyz.someapp.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..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.someapp..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.someapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
你可以在需要切入點表達式的任何地方引用在此切面定義的切入點。例如,要使服務層具有事務性,你可以編寫以下內容:
<aop:config>
<aop:advisor
pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
在基於schema的AOP支持中討論了<aop:config>
和<aop:advisor>
元素。事務管理中討論了事務元素。
實例
Spring AOP用戶可能最常使用execution
切入點指示符。執行表達式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除了返回類型模式(前面的代碼片段中的ret-type-pattern
),名稱模式(name-pattern
)和參數模式(param-pattern
)以外的所有部分都是可選的。返回類型模式確定要匹配連接點、方法的返回類型必須是什麼。最常用作返回類型模式。它匹配任何返回類型。僅當方法返回給定類型時,標準類型名稱才匹配。名稱模式與方法名稱匹配。你可以將通配*
符用作名稱模式的全部或一部分。如果你指定了聲明類型模式,請在其後加上.
將其加入名稱模式組件。參數模式稍微複雜一些:()
匹配不帶參數的方法,而(..)
匹配任意數量(零個或多個)的參數。(*)
模式與採用任何類型的一個參數的方法匹配。(*,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))
不同。如果在運行時傳遞的參數為Serializable,則args版本匹配;如果方法簽名聲明一個類型為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
。有關如何使註解對象在通知正文中可用的信息,請參見“聲明通知”部分。
- 任何採用單個參數的連接點(僅在Spring AOP中是方法執行),並且傳遞的參數的運行時類型具有
@Classified
註解:
@args(com.xyz.security.Classified)
你也可以在綁定形式中使用
@args
。請參閱“聲明通知”部分,如何使通知對象中的註解對象可用。
- 名為
tradeService
的Spring bean上的任何連接點(僅在Spring AOP中執行方法):
bean(tradeService)
- Spring Bean上具有與通配符表達式
* Service
匹配的名稱的任何連接點(僅在Spring AOP中才執行方法):
bean(*Service)
寫一個好的連接點
在編譯期間,AspectJ
處理切入點以優化匹配性能。檢查代碼並確定每個連接點是否(靜態或動態)匹配給定的切入點是一個耗時的過程。(動態匹配意味著無法從靜態分析中完全確定匹配,並且在代碼中進行了測試以確定在運行代碼時是否存在實際匹配)。首次遇到切入點聲明時,AspectJ
將其重寫為匹配過程的最佳形式。這是什麼意思?基本上,切入點以DNF(析取範式)重寫,並且對切入點的組件進行排序,以便首先檢查那些較便宜(消耗最小)的組件。這意味著你不必擔心理解各種切入點指示符的性能,並且可以在切入點聲明中以任何順序提供它們。
但是,AspectJ
只能使用所告訴的內容。為了獲得最佳的匹配性能,你應該考慮他們試圖達到的目標,並在定義中儘可能縮小匹配的搜索空間。現有的指示符自然分為三類之一:同類、作用域和上下文:
-
Kinded
指示器選擇特定類型的連接點:execution
、get
、set
、call
和handler
。 -
Scoping
指示器選擇一組感興趣的連接點(可能是多種類型的):within
和withincode
-
Contextual
指示符根據上下文匹配(和可選綁定):this
、target
和@annotation
編寫正確的切入點至少應包括前兩種類型(Kinded
和Scoping
)。你可以包括上下文指示符以根據連接點上下文進行匹配,也可以綁定該上下文以在通知中使用。僅提供Kinded
的標識符或僅提供Contextual
的標識符是可行的,但是由於額外的處理和分析,可能會影響編織性能(使用的時間和內存)。Scoping
指定符的匹配非常快,使用它們意味著AspectJ
可以非常迅速地消除不應進一步處理的連接點組。一個好的切入點應儘可能包括一個切入點。
參考代碼:
com.liyong.ioccontainer.starter.AopIocContiner
5.4.4 聲明通知
通知與切入點表達式關聯,並且在切入點匹配的方法執行之前、之後或周圍運行。切入點表達式可以是對命名切入點的簡單引用,也可以是在適當位置聲明的切入點表達式。
前置通知
你可以使用@Before
註解在一個切面中聲明前置通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
如果使用就地切入點表達式,則可以將前面的示例重寫為以下示例:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
返回通知
在當匹配方法正常的執行返回時,返回通知運行。你可以使用@AfterReturning
註解進行聲明:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
你可以在同一切面內擁有多個通知聲明(以及其他成員)。在這些示例中,我們僅顯示單個通知聲明,以及其中每個通知的效果。
有時,你需要在通知正文中訪問返回的實際值。你可以使用@AfterReturning
的形式綁定返回值以獲取該訪問,如以下示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
返回屬性中使用的名稱必須與advice
方法中的參數名稱相對應。當方法執行返回時,返回值將作為相應的參數值傳遞到通知方法。returning
也將匹配限制為僅返回指定類型值的方法執行(在這種情況下為Object,它匹配任何返回值)。
請注意,當使用返回後通知時,不可能返回完全不同的引用。
異常後置通知
在拋異常通知後,當匹配的方法執行通過拋出異常退出時運行。你可以使用@AfterThrowing
註解進行聲明,如以下示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
通常,你希望通知僅在引發給定類型的異常時才運行,並且你通常還需要訪問通知正文中的引發異常。你可以使用throwing
屬性來限制匹配(如果需要)(否則,請使用Throwable
作為異常類型),並將拋出的異常綁定到通知的參數。以下示例顯示瞭如何執行此操作:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
throwing
屬性中使用的名稱必須與通知方法中的參數名稱相對應。當通過拋出異常退出方法執行時,該異常將作為相應的參數值傳遞給通知的方法。throwing
還將匹配僅限制為拋出指定類型的異常(在這種情況下為DataAccessException
)的方法執行。
最終通知
當匹配的方法執行退出時,通知(最終)運行。通過使用@After
註解聲明它。之後必須準備處理正常和異常返回條件的通知。它通常用於釋放資源和類似目的。以下示例顯示了最終通知的用法:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
環繞通知
最後一種通知是環繞通知。環繞通知在匹配方法的執行過程中“環繞”運行。它有機會在方法執行之前和之後執行工作,並確定何時、如何執行,甚至是否真的執行方法。如果需要以線程安全的方式(例如,啟動和停止計時器)在方法執行之前和之後共享狀態,則通常使用環繞通知。始終使用能力最小的通知來滿足你的要求(也就是說,在通知可以使前置通知時,請勿用環繞通知)。
通過使用@Around
註解來聲明環繞通知。通知方法的第一個參數必須是ProceedingJoinPoint
類型。在通知的正文中,在ProceedingJoinPoint
上調用proceed()
會使底層(真正的執行方法)方法執行。proceed
方法也可以傳入Object []
。數組中的值用作方法執行時的參數。
當用
Object []
進行調用時,proceed
的行為與AspectJ
編譯器所編譯的around
通知的proceed
為略有不同。對於使用傳統AspectJ
語言編寫的環繞通知,傳遞給proceed
的參數數量必須與傳遞給環繞通知的參數數量(而不是基礎連接點採用的參數數量)相匹配,並且傳遞給給定的參數位置會取代該值綁定到的實體的連接點處的原始值(不要擔心,如果這現在沒有意義)。Spring採取的方法更簡單,並且更適合其基於代理的,僅執行的語義。如果你編譯為Spring編寫的@AspectJ
切面,並在AspectJ
編譯器和weaver
中使用參數進行處理,則只需要意識到這種區別。有一種方法可以在Spring AOP和AspectJ
之間100%兼容,並且在下面有關通知參數的部分中對此進行了討論。
以下示例顯示瞭如何使用環繞通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
環繞通知返回的值是該方法的調用者看到的返回值。例如,如果一個簡單的緩存切面有一個值,則它可以從緩存中返回一個值,如果沒有,則調用proceed()
。請注意,在環繞通知的正文中,proceed
可能被調用一次,多次或完全不被調用。所有這些都是合法的。
參考代碼:
com.liyong.ioccontainer.starter.AopIocContiner
通知參數
Spring提供了完全類型化的通知,這意味著你可以在通知簽名中聲明所需的參數(如我們先前在返回和拋出示例中所看到的),而不是一直使用Object []
數組。我們將在本節的後面部分介紹如何使參數和其他上下文值可用於通知主體。首先,我們看一下如何編寫通用通知,以瞭解該通知當前通知的方法。
獲取當前JoinPoint
任何通知方法都可以將org.aspectj.lang.JoinPoint
類型的參數聲明為其第一個參數。請注意,環繞通知聲明ProceedingJoinPoint
類型為第一個參數,該參數是JoinPoint
的子類。JoinPoint
接口提供了許多有用的方法:
-
getArgs()
: 返回方法參數。 -
getThis()
: 返回代理對象。 -
getTarget()
: 返回目標對象。 -
getSignature()
: 返回通知使用的方法的描述。 -
toString()
: 打印有關所有通知方法的有用描述。
有關更多詳細信息,請參見javadoc。
傳遞參數給通知
我們已經看到了如何綁定返回的值或異常值(在返回之後和引發通知之後)。要使參數值可用於通知正文,可以使用args
的綁定形式。如果在args
表達式中使用參數名稱代替類型名稱,則在調用通知時會將相應參數的值作為參數值傳遞。一個例子應該使這一點更清楚。假設你要通知以Account
對象作為第一個參數的DAO
操作的執行,並且你需要在通知正文中訪問該帳戶。你可以編寫以下內容:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
切入點表達式的args(account,..)
部分有兩個用途。首先,它將匹配限制為僅方法採用至少一個參數且傳遞給該參數的參數為Account
實例的那些方法執行。其次,它通過account
參數使實際的Account
對象可用於通知。
寫這個的另一種方法是聲明一個切入點,當它匹配一個連接點時提供Account
對象值,然後從通知中引用命名的切入點。如下所示:
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
有關更多詳細信息,請參見AspectJ
編程指南。
代理對象(this
)、目標對象(target
)和註解(@within
,@target
,@annotation
和@args
)都可以以類似的方式綁定。接下來的兩個示例顯示如何匹配使用@Auditable
註解的方法的執行並提取審計代碼:
這兩個示例中的第一個顯示了@Auditable
註解的定義:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
這兩個示例中的第二個示例顯示了與@Auditable
方法的執行相匹配的通知:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
通知參數和泛型
Spring AOP可以處理類聲明和方法參數中使用的泛型。假設你具有如下通用類型:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
你可以通過在要攔截方法的參數類型中鍵入advice參數,將方法類型的攔截限制為某些參數類型:
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
這種方法不適用於泛型集合。因此,你不能按以下方式定義切入點:
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
為了使這項工作有效,我們將不得不檢查集合的每個元素,這是不合理的,因為我們也無法決定通常如何處理null
。要實現類似的目的,你必須將參數鍵入Collection <?>
並手動檢查元素的類型。
確定參數名稱
通知調用中的參數綁定依賴於切入點表達式中使用的名稱與通知和切入點方法簽名中聲明的參數名稱的匹配。
通過Java反射無法獲得參數名稱,因此Spring AOP使用以下策略來確定參數名稱:
- 如果用戶已明確指定參數名稱,則使用指定的參數名稱。通知和切入點註解均具有可選的
argNames
屬性,你可以使用該屬性來指定帶註解的方法的參數名稱。這些參數名稱在運行時可用。以下示例顯示如何使用argNames
屬性:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}
如果第一個參數是JoinPoint
、ProceedingJoinPoint
或JoinPoint.StaticPart
類型,則可以從argNames
屬性的值中忽略該參數的名稱。例如,如果你修改前面的通知以接收連接點對象,則argNames
屬性不需要包括它:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}
對JoinPoint
、ProceedingJoinPoint
和JoinPoint.StaticPart
類型的第一個參數給予的特殊處理對於不收集任何其他連接點上下文的通知實例特別方便。在這種情況下,你可以省略argNames
屬性。例如,以下通知無需聲明argNames
屬性:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
- 使用'
argNames
'屬性有點笨拙,因此,如果未指定'argNames
'屬性,Spring AOP將查找該類的調試信息,並嘗試從局部變量表中確定參數名稱。只要已使用調試信息(至少是-g:vars
)編譯了類,此信息就會存在。 啟用此標誌時進行編譯的後果是:(1)你的代碼稍微易於理解(逆向工程),(2)類文件的大小略大(通常無關緊要),(3)編譯器未應用刪除未使用的局部變量的優化。換句話說,通過啟用該標誌,你應該不會遇到任何困難。
如果即使沒有調試信息,
AspectJ
編譯器(ajc)都已編譯@AspectJ
切面,則無需添加argNames
屬性,因為編譯器會保留所需的信息。
- 如果在沒有必要調試信息的情況下編譯了代碼,Spring AOP將嘗試推斷綁定變量與參數的配對(例如,如果切入點表達式中僅綁定了一個變量,並且advice方法僅接受一個參數,則配對很明顯)。如果在給定可用信息的情況下變量的綁定不明確,則拋出
AmbiguousBindingException
。 - 如果以上所有策略均失敗,則拋出
IllegalArgumentException
。
proceed參數
前面我們提到過,我們將描述如何編寫一個在Spring AOP和AspectJ
中始終有效的參數的proceed
調用。解決方案是確保通知簽名按順序綁定每個方法參數。以下示例顯示瞭如何執行此操作:
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
在許多情況下,無論如何都要進行此綁定(如上例所示)。
通知順序
當多條通知都希望在同一連接點上運行時會發生什麼? Spring AOP遵循與AspectJ
相同的優先級規則來確定通知執行的順序。優先級最高的通知在進入時首先運行(因此,給定兩個before
通知,優先級最高的通知首先運行)。在從連接點出來的過程中,優先級最高的通知最後運行(因此,給定兩個after
通知,優先級最高的通知將排在第二)。
在不同切面定義的兩個通知都需要在同一個連接點上運行時,除非另行指定,否則執行順序是未定義的。你可以通過指定優先級來控制執行順序。通過在切面類中實現org.springframework.core.Ordered
接口或使用Order註解對其進行註解,可以通過常規的Spring方法來完成。給定兩個切面,從Ordered.getValue()
(或註解值)返回較低值的切面具有較高的優先級
當在同一個切面中定義的兩個通知都需要在同一個連接點上運行時,順序是未定義的(因為無法通過java編譯類的反射檢索聲明順序)。考慮將此類通知方法分解為每個切面類中的每個連接點的一個通知方法,或者將通知片段重構為可以在切面級別排序的單獨切面類。
5.4.5 引入
引入(在AspectJ中稱為類型間聲明)使能夠聲明已通知的對象實現給定接口,並代表這些對象提供該接口的實現。
你可以使用@DeclareParents
註解進行介紹。此註解用於聲明匹配類型具有新的父類(因此具有名稱)。例如,給定一個名為UsageTracked
的接口和該接口的一個名為DefaultUsageTracked
的實現,下面的切面聲明瞭服務接口的所有實現者也實現了UsageTracked
接口(例如通過JMX
公開統計信息):
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
要實現的接口由帶註解的字段的類型確定。@DeclareParents
註解的value
屬性是AspectJ
類型的模式。匹配類型的任何bean都實現UsageTracked
接口。注意,在前面示例的before
通知中,服務bean可以直接用作UsageTracked
接口的實現。如果以編程方式訪問bean,則應編寫以下內容:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
參考代碼:
com.liyong.ioccontainer.starter.AopDeclareParentsIocContiner
5.4.6 切面實例化模型
這是一個高級主題。如果你剛開始使用AOP,則可以放心地跳過它,直到以後。
默認情況下,應用程序上下文中每個切面都有一個實例。 AspectJ將此稱為單例實例化模型。可以使用bean生命週期來定義切面。Spring支持AspectJ的perthis
和pertarget
實例化模型(當前不支持percflow
,percflowbelow
和pertypewithin
)。
你可以通過在@Aspect
註解中指定perthis
來聲明perthis
切面。考慮以下示例:
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {
private int someState;
@Before(com.xyz.myapp.SystemArchitecture.businessService())
public void recordServiceUsage() {
// ...
}
}
在前面的示例中,“ perthis
”子句的作用是為每個執行業務服務的唯一服務對象(每個與切入點表達式匹配的連接點綁定到“ this
”的唯一對象)創建一個切面實例。切面實例是在服務對象上首次調用方法時創建的。當服務對象超出範圍時,切面將超出範圍。在創建切面實例之前,其中的任何通知都不會執行。一旦創建了切面實例,在其中聲明的通知就會在匹配的連接點上執行,但僅當服務對象與此切面相關聯時才執行。有關每個子句的更多信息,請參見AspectJ編程指南。
pertarget
實例化模型的工作方式與perthis
完全相同,但是它在匹配的連接點為每個唯一目標對象創建一個切面實例。
5.4.7 AOP例子
現在你已經瞭解了所有組成部分是如何工作的,我們可以將它們組合在一起做一些有用的事情。
有時由於併發問題(例如,死鎖失敗),業務服務的執行可能會失敗。如果重試該操作,則很可能在下一次嘗試中成功。對於適合在這種情況下重試的業務服務(不需要為解決衝突而需要返回給用戶的冪等操作),我們希望透明地重試該操作,以避免客戶端看到PessimisticLockingFailureException
。這是一個明顯跨越服務層中的多個服務的需求,因此非常適合通過切面實現。
因為我們想重試該操作,所以我們需要使用環繞通知,以便可以多次調用proceed
。以下清單顯示了基本切面的實現:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
請注意,切面實現了Ordered
接口,以便我們可以將切面的優先級設置為高於事務通知的優先級(每次重試時都需要一個新的事務)。maxRetries
和order
屬性均由Spring配置。通知的主要動作發生在doConcurrentOperation
中。請注意,目前,我們將重試邏輯應用於每個businessService()
。我們嘗試繼續,如果失敗並出現PessimisticLockingFailureException
,則我們將再次重試,除非我們用盡了所有重試嘗試。
對應的Spring配置如下:
<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
為了完善切面,使其僅重試冪等操作,我們可以定義以下冪等註解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
然後,我們可以使用註解來註釋服務操作的實現。切面更改為僅重試冪等操作涉及更改切入點表達式,以便僅@Idempotent
操作匹配,如下所示:
@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// ...
}
作者
個人從事金融行業,就職過易極付、思建科技、某網約車平臺等重慶一流技術團隊,目前就職於某銀行負責統一支付系統建設。自身對金融行業有強烈的愛好。同時也實踐大數據、數據存儲、自動化集成和部署、分佈式微服務、響應式編程、人工智能等領域。同時也熱衷於技術分享創立公眾號和博客站點對知識體系進行分享。關注公眾號:青年IT男 獲取最新技術文章推送!
博客地址: http://youngitman.tech
CSDN: https://blog.csdn.net/liyong1028826685
微信公眾號:
技術交流群: