5.1 介紹
AOP(Aspect-Oriented Progerammint) 是對OOP編程的一種補充. OOP編程的關(guān)鍵單元是類(Class), 而AOP編程的關(guān)鍵單元是切面(aspect),方面支持關(guān)注點模塊化,如橫跨多個類和對象的事務(wù)管理, 這些關(guān)注點在AOP中常被稱為橫切關(guān)注點.
AOP框架是Spring的一個關(guān)鍵組件,雖然Spring IoC容器不依賴AOP铣口,這意味著如果您不想使用AOP,就不需要使用AOP,但是AOP補充了Spring IoC,提供了一個非常有用的中間件解決方案。
在spring 2.0中引入了基于xml或@AspectJ注解的方式使用AOP
AOP在spring框架中的使用:
- 提供聲明性企業(yè)服務(wù)振惰,特別是作為EJB聲明性服務(wù)的替代。最重要的服務(wù)是聲明性事務(wù)管理。
- 允許用戶實現(xiàn)自定義方面凿叠,對OOP進行補充。
5.1.1 AOP概念
- Aspect: 切面, 即跨多個類的關(guān)注點模塊嚼吞。Java企業(yè)應用程序中的事務(wù)管理就是一個很好的例子盒件。在Spring AOP中,Aspect是使用常規(guī)類(基于xml的方法)或使用@Aspect注釋的常規(guī)類(@AspectJ樣式)實現(xiàn)的舱禽。
- Join point: 連接點, 即程序執(zhí)行過程中的一個點炒刁,如方法的執(zhí)行或異常的處理, 在Spring AOP中,連接點總是用一個方法表示誊稚。
- Advice: 通知, 切面在特定的連接點上執(zhí)行的操作.分為
around, before, after
, 許多AOP框架翔始,包括Spring,都將通知建模為攔截器里伯,維護一個圍繞連接點的攔截器鏈城瞎。 - Pointcut: 切入點, 即匹配連接點的定義.通知與切入點表達式相關(guān)聯(lián),并在與切入點匹配的任何連接點上運行, 連接點與切入點表達式匹配的概念是AOP的核心疾瓮,Spring默認使用AspectJ切入點表達式語言脖镀。
- Introduction: 引入, 為一個類添加新的方法或字段, Spring AOP允許您向任何被通知的對象引入新的接口(以及相應的實現(xiàn)), 例如,可以通過引入使bean實現(xiàn)IsModified接口狼电,以簡化緩存.
- Target object: 目標對象, 被一個或多個切面通知的對象, 也稱為被通知對象认然。由于Spring AOP是使用運行時代理實現(xiàn)的补憾,所以這個對象將始終是代理對象。
- AOP proxy: AOP代理, AOP框架為了實現(xiàn)方面契約(建議方法執(zhí)行等等)而創(chuàng)建的一個對象卷员,在Spring框架中盈匾,AOP代理為JDK動態(tài)代理或CGLIB代理。
- Weaving: 織入, 即將切面與其他應用程序類型或?qū)ο箧溄颖下猓詣?chuàng)建通知的對象. 這可以在編譯時(例如削饵,使用AspectJ編譯器)、加載時或運行時完成未巫。與其他純Java AOP框架一樣窿撬,Spring AOP在運行時完成。
通知的類型:
- Before advice: 在連接點之前執(zhí)行的通知叙凡,但不能阻止執(zhí)行流繼續(xù)到連接點(除非拋出異常)劈伴。
- After (finally) advice: 無論連接點以何種方式退出,都會執(zhí)行的通知.
- After throwing advice: 如果方法拋出異常,則要執(zhí)行的通知握爷。
- After returning advice: 在連接點正常完成后執(zhí)行的通知
- Around advice: 圍繞連接點的通知跛璧。Around通知可以在連接點調(diào)用前后執(zhí)行自定義行為。它還負責選擇是繼續(xù)到連接點新啼,還是通過返回它自己的返回值或拋出異常來簡化通知的方法執(zhí)行追城。
環(huán)繞通知是功能最強的通知, 在AOP以及其他AOP框架中都提供了完整的通知類型, 但建議在使用通知時盡可能使用功能最弱的通知, 例如,如果只需要用方法的返回值更新緩存燥撞,那么最好實現(xiàn)after return通知座柱,而不是around通知,盡管around通知可以完成相同的任務(wù)物舒。使用最合適的通知類型可以提供更簡單的編程模型色洞,出錯的可能性更小。
5.1.2 Spring AOP的功能和目標
Spring AOP是用純Java實現(xiàn)的冠胯。Spring AOP目前只支持方法作為連接點(建議在Spring bean上執(zhí)行方法). 沒有實現(xiàn)字段攔截锋玲,如果需要建議字段訪問和更新連接點,請考慮AspectJ之類的語言涵叮。
5.1.3 AOP代理
AOP默認使用標準JDK動態(tài)代理。這允許代理任何接口(或一組接口)伞插。
Spring AOP還可以使用CGLIB代理割粮。這對于代理類是必要的, 如果業(yè)務(wù)對象沒有實現(xiàn)接口,則默認使用CGLIB媚污。
5.2 @AspectJ支持
@AspectJ是一個將常規(guī)JAVA類聲明為切面的注解. @AspectJ樣式是AspectJ項目作為AspectJ 5發(fā)行版的一部分引入的舀瓢。Spring使用了與AspectJ 5相同的注解樣式, 但是Spring AOP運行時仍然是純Spring AOP,并且不依賴于AspectJ編譯器或編織器耗美。
5.2.1 開啟@AspectJ支持
可以通過XML或Java樣式配置啟用@AspectJ支持京髓。在這兩種情況下航缀,還需要確保AspectJ的aspectjweaver.jar庫位于應用程序的類路徑上(版本1.8或更高).
java方式開啟:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
xml方式開啟:
<aop:aspectj-autoproxy/>
5.2.2 聲明切面
啟用@AspectJ支持后,在應用程序上下文中使用@AspectJ方面(具有@Aspect注釋)類定義的任何bean都將被Spring自動檢測到堰怨,并用于配置Spring AOP芥玉。
示例:
應用程序上下文中的常規(guī)bean定義,指向具有@Aspect注釋的bean類
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of aspect here as normal -->
</bean>
//
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
切面也可以通過組件掃描自動發(fā)現(xiàn), 由于組件掃描不能自動發(fā)現(xiàn)@Aspect注解, 因此要加上@Component注解.
在Spring AOP中备图,不可能讓方面本身成為來自其他方面的通知的目標灿巧。類上的@Aspect注釋將其標記為一個方面,spring會將其排除在自動代理之外揽涮。
5.2.3 聲明切入點
Spring AOP只支持Spring bean的方法執(zhí)行連接點抠藕,所以可以將切入點看作是匹配Spring bean上方法的執(zhí)行。切入點聲明有兩部分:一個包含名稱和任何參數(shù)的簽名蒋困,以及一個確定我們對哪個方法執(zhí)行感興趣的切入點表達式盾似。在AOP的@AspectJ注釋風格中,切入點簽名由一個正則方法定義提供雪标,切入點表達式使用@Pointcut注釋表示(作為切入點簽名的方法必須有一個void返回類型)零院。
下面的例子定義了一個名為“anyOldTransfer”的切入點,它將匹配任何名為“transfer”的方法的執(zhí)行:
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature
切入點標識符
Spring AOP支持在切入點表達式中使用的以下AspectJ切入點標識符(PCD):
- execution: 匹配方法執(zhí)行連接點.
- within: 將匹配限制為特定類型中的連接點(當使用Spring AOP時汰聋,只需執(zhí)行在匹配類型中聲明的方法)
- this: 將匹配限制為連接點(使用Spring AOP時方法的執(zhí)行)门粪,其中bean引用(Spring AOP代理)是給定類型的實例
- target: 將匹配限制為連接點(使用Spring AOP時方法的執(zhí)行),其中目標對象(代理的應用程序?qū)ο?是給定類型的實例
- args: 將匹配限制為連接點(使用Spring AOP時方法的執(zhí)行)烹困,其中的參數(shù)是給定類型的實例
- @target: 將匹配限制為連接點(使用Spring AOP時方法的執(zhí)行)玄妈,其中執(zhí)行對象的類具有給定類型的注釋
- @args: 將匹配限制為連接點(使用Spring AOP時方法的執(zhí)行),其中傳遞的實際參數(shù)的運行時類型具有給定類型的注釋
- @within: 限制對具有給定注釋的類型中的連接點的匹配(使用Spring AOP時髓梅,使用給定注釋在類型中聲明的方法的執(zhí)行)
- @annotation: 將匹配限制為連接點的主題(在Spring AOP中執(zhí)行的方法)具有給定注釋的連接點
Spring AOP還支持一個名為bean的PCD, 這允許您將連接點的匹配限制為特定名稱的一個或一組bean(使用通配符時)拟蜻。其定義格式為:
bean(idOrNameOfBean)
idOrNameOfBean
可以是任何spring定義的bean. 通配符*
支持有限的通配,還可以使用&&, ||, !
.
請注意,bean PCD只在Spring AOP中受支持枯饿,而在AspectJ編織中不受支持.
bean PCD在實例級(基于Spring bean名稱概念)而不是僅在類型級進行操作酝锅。
組合切點表達式
切入點表達式可以使用“&&”、“||”和“!”組合奢方。還可以通過名稱引用切入點表達式搔扁。下面的例子顯示了三個切入點表達式:anyPublicOperation(如果方法執(zhí)行連接點表示任何公共方法的執(zhí)行,則匹配該表達式);inTrading(如果方法執(zhí)行在交易模塊中蟋字,它就匹配)和tradingOperation(如果方法執(zhí)行代表交易模塊中的任何公共方法稿蹲,它就匹配)。
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
// 通過名稱引入
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
最好的實踐是用上面所示的較小的命名組件構(gòu)建更復雜的切入點表達式鹊奖。當按名稱引用切入點時苛聘,應用普通的Java可見性規(guī)則(您可以看到相同類型的私有切入點、層次結(jié)構(gòu)中的受保護切入點、任何地方的公共切入點设哗,等等)唱捣。可見性不影響切入點匹配网梢。
定義共享的公共切入點
在處理企業(yè)應用程序時震缭,您通常希望從幾個方面引用應用程序的模塊和特定的操作集。我們建議定義一個“SystemArchitecture”方面澎粟,它捕獲用于此目的的公共切入點表達式.
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/**
* web層的連接點, 在com.xyz.someapp.web包或其子包中定義連接點
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}
/**
* service層定義的連接點,連接點定義在com.xyz.someapp.service包及其子包中
*/
@Pointcut("within(com.xyz.someapp.service..*)")
public void inServiceLayer() {}
/**
* dao層接連點, 在com.xyz.someapp.dao包及其子包中定義
*/
@Pointcut("within(com.xyz.someapp.dao..*)")
public void inDataAccessLayer() {}
/**
*
* 如果按功能區(qū)域?qū)ervice 接口進行分組 (如在com.xyz.someapp.abc.service and com.xyz.someapp.def.service)
* 那么這個切點表達式為"execution(* com.xyz.someapp..service.*.*(..))"
* 你也可以用bean PCD來定義表達式,即"bean(*Service)".此處的前提是你的service bean的命名方式一致.
*/
@Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
public void businessService() {}
/**
* 假設(shè)這個dao接口定義在 "dao" 包中, 它的實現(xiàn)類定義在其子包中
*/
@Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
在這樣一個方面中定義的切入點可以在任何需要切入點表達式的地方引用蛀序。例如,要使服務(wù)層具有事務(wù)性:
<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>
示例
Spring AOP用戶可能最經(jīng)常使用execution 切入點標識符活烙。其格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除了ret-type-pattern, name-pattern, param-pattern之外, 其他都是可選的.
ret-type確定了匹配的連接點方法的返回類型.最常見的情況是使用作為返回類型模式徐裸,它匹配任何返回類型。只有當方法返回給定類型時啸盏,才會匹配完全限定類型名稱重贺。
name表示匹配的方法名稱, 可以使用通配符作為名稱的全部或部分。
param表示參數(shù), ()
表示無參, (..)
匹配任何參數(shù)(0個或多個). (*)
匹配一個參數(shù)(類型不限), (*, String)
匹配兩個參數(shù), 第一個可以是任何類型, 第二個必須 是String.
常見切入點表達式的例子:
執(zhí)行任何公共方法:
execution(public * *(..))
執(zhí)行任何以set開頭的方法:
execution(* set*(..))
執(zhí)行在AccountService
接口中定義的任何方法:
execution(* com.xyz.service.AccountService.*(..))
執(zhí)行定義在service
包中的任何方法:
execution(* com.xyz.service.*.*(..))
執(zhí)行定義在service
包及其子包中的任何方法:
execution(* com.xyz.service..*.*(..))
在service
包中的任何連接點:
within(com.xyz.service.*)
在service
包及其子包中的任何連接點:
within(com.xyz.service..*)
代理實現(xiàn)AccountService接口的任何連接點(僅在Spring AOP中執(zhí)行方法):
this(com.xyz.service.AccountService)
AccountService接口實現(xiàn)中定義的任何連接點(僅在Spring AOP中執(zhí)行方法):
target(com.xyz.service.AccountService)
只含有一個可序列化的參數(shù)的連接點:
args(java.io.Serializable)
這與execution(* *(java.io.Serializable))
不同, args版本是如果在運行時傳遞的參數(shù)是可序列化的,則匹配. 后者是如果方法簽名聲明一個Serializable類型的參數(shù)則匹配.
目標對象上聲明了@Transactional
注解的連接點:
@target(org.springframework.transaction.annotation.Transactional)
目標對象的聲明類型具有@Transactional注釋的任何連接點:
@within(org.springframework.transaction.annotation.Transactional)
任何連接點(只在Spring AOP中執(zhí)行方法)回懦,其中執(zhí)行方法具有@Transactional注釋:
@annotation(org.springframework.transaction.annotation.Transactional)
任何接受單個參數(shù)的連接點(僅在Spring AOP中執(zhí)行方法)气笙,其中傳遞的參數(shù)的運行時類型有@ classification注釋:
@args(com.xyz.security.Classified)
在名為tradeService的Spring bean中定義的任何連接點(僅在Spring AOP中執(zhí)行方法):
bean(tradeService)
在bean名稱是以Service結(jié)尾的bean中定義的連接點:
bean(*Service)
編寫好的切入點
在編譯期間,AspectJ處理切入點時會嘗試優(yōu)化匹配性能怯晕。檢查代碼并確定每個連接點是否匹配(靜態(tài)或動態(tài))給定的切入點是一個代價高昂的過程潜圃。在第一次遇到切入點聲明時,AspectJ將把它重寫為匹配過程的最佳形式舟茶√菲冢基本上切入點是用DNF(Disjunctive Normal Form)重寫的,切入點的組件被排序吧凉,以便首先檢查那些計算成本更低的組件隧出。這意味著您不必擔心理解各種切入點設(shè)計器的性能,并且可以在切入點聲明中以任意順序提供它們阀捅。
然而胀瞪,AspectJ只能處理它被告知的內(nèi)容,為了獲得最佳匹配性能饲鄙,應該考慮它們試圖實現(xiàn)什么凄诞,并在定義中盡可能縮小匹配的搜索空間。現(xiàn)有的標識符可以分為三類:kinded忍级、scoping和context:
- kinded: 表示選擇一種特定類型的連接點.如
execution, get, set, call, handler
. - scoping: 選擇一組感興趣的連接點(可能有多種).如
within, withincode
. - context; 根據(jù)context來匹配的.如
this, target, @annotation
.
一個編寫良好的切入點應該嘗試至少包含前兩種類型(kinded和scoping)帆谍,而如果希望基于連接點上下文進行匹配,或者綁定上下文以便在通知中使用颤练,則可以包含上下文指示符。提供一個kinded類型的指示符或context類的標識符都可以,但是由于所有額外的處理和分析嗦玖,可能會影響編織性能(使用的時間和內(nèi)存)患雇。scoping類的標識符匹配起來非常快宇挫,而且它們的使用意味著AspectJ可以非晨林ǎ快地取消不應該進一步處理的連接點組——這就是為什么一個好的切入點應該總是包含一個連接點(如果可能的話)。
5.2.4 聲明通知
通知與切入點表達式相關(guān)聯(lián)器瘪,并在切入點匹配的方法執(zhí)行之前翠储、之后或前后運行. 切入點表達式可以是對指定切入點的簡單引用,也可以是在適當位置聲明的切入點表達式橡疼。
Before通知
Before通知在切面中使用@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() {
// ...
}
}
有時候援所,您需要在advice主體中訪問返回的實際值。您可以在@Afterreturn中綁定這個返回值:
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) {
// ...
}
}
returning
屬性中使用的名稱必須與advice方法中的參數(shù)名稱對應欣除。returning
子句還將匹配限制為只匹配那些返回指定類型值的方法執(zhí)行.
后置異常通知
使用@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作為異常類型),并將拋出的異常綁定到通知參數(shù)挽牢。
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
屬性中使用的名稱必須與advice方法中的參數(shù)名稱相對應谱煤。throwing
子句還限制只匹配那些拋出指定類型異常的方法執(zhí)行(本例中為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() {
// ...
}
}
環(huán)繞通知
它在連接點方法執(zhí)行之前和之后執(zhí)行禽拔,并確定何時刘离、如何、甚至是否真正執(zhí)行連接點方法奏赘。如果需要以線程安全的方式(例如啟動和停止計時器)共享方法執(zhí)行前后的狀態(tài)寥闪,通常會使用Around建議。
使用@Around
聲明.advice方法的第一個參數(shù)必須是ProceedingJoinpoint
類型,
在通知的方法體中磨淌,調(diào)用對ProceedingJoinpoint的proceed()會執(zhí)行連接點方法疲憋。
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;
}
}
around通知的返回值將是方法調(diào)用者得到的返回值, 請注意,proceed可以被調(diào)用一次梁只、多次缚柳,或者根本不在around建議的主體中調(diào)用.
通知參數(shù)
任何advice方法都可以聲明一個org.aspectj.lang.JoinPoint類型的參數(shù)為它的第一個參數(shù),請注意搪锣,around通知的第一個參數(shù)要求是proceedingJoinpoint類型秋忙,它是JoinPoint的子類。JoinPoint接口提供了許多有用的方法构舟,比如getArgs()(返回方法參數(shù))灰追、getThis()(返回代理對象)、getTarget()(返回目標對象)、getSignature()(返回被建議的方法的簽名)和toString()(打印被建議的方法的有用描述)弹澎。
給通知傳遞參數(shù)
要使參數(shù)值對通知主體可用朴下,可以使用args的綁定形式.
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
切入點表達式的args(account,..)
部分有兩個目的:首先苦蒿,通知方法至少接受一個參數(shù)殴胧,并且傳遞給該參數(shù)的參數(shù)是Account類型的一個實例;其次,它通過Account參數(shù)使實際的Account對象對通知可用佩迟。
另一種編寫方法是聲明一個切入點团滥,該切入點在匹配連接點時“提供”Account對象值,然后僅引用通知中的指定切入點名稱报强。
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
代理對象(this)灸姊、目標對象(target)和注釋(@within、@target躺涝、@annotation厨钻、@args)都可以以類似的方式綁定。下面的示例展示了如何匹配使用@Auditable注釋注釋的方法的執(zhí)行坚嗜。
首先定義@Auditable注釋:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
然后匹配執(zhí)行@Auditable方法的通知:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
通知參數(shù)和泛型
假設(shè)您有這樣一個泛型類型:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
您可以將方法類型的攔截限制為特定的參數(shù)類型夯膀,只需將advice參數(shù)鍵入要攔截方法的參數(shù)類型即可:
@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
}
為了實現(xiàn)這一點,我們必須檢查集合的每個元素碟绑,這是不合理的俺猿,因為我們也不能決定如何處理空值。要實現(xiàn)類似的功能格仲,必須將參數(shù)鍵入Collection<?>
并手動檢查元素的類型押袍。
確定參數(shù)名稱
Spring AOP的參數(shù)名稱是不能通過反射來獲取的,而是通過切點表達式中聲明的參數(shù)與切點方法的參數(shù)名稱來匹配的, 因此spring定義了以下規(guī)則:
- 在切點表達式中通過
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
}
如果方法的第一個參數(shù)是JoinPoint, ProceedingJoinPoint, JoinPoint.StaticPart
類型, 則可省略這個參數(shù)的參數(shù)名稱(如果只有一個且是以上類型的參數(shù), 則可省略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
}
- 不指定參數(shù)名稱, 可以用debug模式進行編譯, spring aop將根據(jù)debug模式下的本地變量來確定參數(shù).
- 如果在編譯時沒有包含必需的調(diào)試信息, spring aop也會推斷參數(shù)的綁定信息(如只有一個參數(shù)時,那么參數(shù)的綁定是很明確的), 如果無法確定參數(shù), 則會拋出
AmbiguousBindingException
. - 如果以上策略都失敗了, 則會拋出
IllegalArgumentException
. - 通過
execution()
表達式.
通知的執(zhí)行順序
當在一個連接點方法上執(zhí)行多個通知時會發(fā)生什么呢? spring遵循與AspectJ
相同的通知執(zhí)行優(yōu)先級規(guī)則, 對于進入通知(如before通知), 最高優(yōu)先級的通知最先執(zhí)行,對于退出通知(如after通知), 優(yōu)先級最高的最后執(zhí)行.
當兩個在不同切面定義的通知運行在同一個連接點上, 如果沒有指定, 則其運行順序是不確定的. 可以通過實現(xiàn)Ordered
接口或@Order
注解來定義順序,較小的值具有高的優(yōu)先級.
在同一個切面中定義的兩個通知運行在同一個連接點上,其執(zhí)行順序也是未定義的,因為無法通過反射來確定注解的編譯順序,此時考慮分開定義并指定其執(zhí)行順序.
5.4.5 引入
引入允許一個切面聲明某個通知對象實現(xiàn)一個指定的接口并代表這個通知對象實現(xiàn)這個接口.
@DeclareParents
這個注解聲明了被注解的類具有一個新的父類,例如有一個UsageTracked
接口, 其有一個實現(xiàn)DefaultUsageTracked
, 則下面的切面聲明了所有實現(xiàn)了service
的接口也都實現(xiàn)UsageTracked
接口.
@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
都將實現(xiàn)UsageTracked
接口, defaultImpl
屬性指定了實現(xiàn)類.
5.4.6 切面實例化
默認情況下, 切面實例在容器中是單例的, AspectJ
調(diào)用這些單例模塊. 但是也可以定義可選生命周期的切面.通過prethis
或pertarget
指定.
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {
private int someState;
@Before(com.xyz.myapp.SystemArchitecture.businessService())
public void recordServiceUsage() {
// ...
}
}
在上面的例子中, perthis
的作用是為每個執(zhí)行businessService
的唯一service對象創(chuàng)建一個切面實例.(這個唯一對象將會通過連接點上的切點表達式綁定this
).切面實例將在第一次調(diào)用這個方法時被創(chuàng)建.
5.4.7 示例
在并發(fā)條件下, 有些業(yè)務(wù)操作可能會執(zhí)行失敗, 如果再次重新執(zhí)行則可能成功, 此時我們可以通過around
通知來操作.
@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;
}
}
由于它實現(xiàn)了Ordered
接口, 因此可以切面的優(yōu)先級高于事務(wù)通知的優(yōu)先級(每次重試都需要一個新的事務(wù)).
xml配置如下:
<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
如果只是進行重試冪等操作, 我們可以定義Idempotent
注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
可以使用這個注解杰注釋service操作的實現(xiàn).
@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
...
}
5.5 基于XML的AOP支持
在xml配置中, 要提供aop
命名空間.要引入spring-aop
schema.
在xml配置中, 所有的aspect
和advisor
元素都必須包含在<aop:config/>
元素中.
<aop:config>
配置大量的使用了spring的自動代理機制, 如果你通過BeanNameAutoProxy
或類似的方式顯式的指定了自動代理, 這可能會引發(fā)一些問題.因此永遠不要混合使用它們.
5.5.1聲明切面
切面是一個被聲明為bean的常規(guī)的java對象.
通過<aop:aspect>
元素聲明一個切面并通過其ref
屬性引用一個bean.
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
5.5.2 聲明切入點<aop:pointcut>
可以在<aop:config>
元素中聲明一個切入點, 使其可以在多個切面中共享.
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
</aop:config>
或者:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.myapp.SystemArchitecture.businessService()"/>
</aop:config>
在切面中聲明切入點:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
與@AspectJ
切面類似, 使用xml配置也可以包含切入點上下文,
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
public void monitor(Object service) {
...
}
在xml配置中, 在切點子表達式中定義&&, ||, !
可以使用and, or, not
替換.
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) and this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
注意,以這種方式定義的切入點由它們的XML id引用凯肋,不能作為命名切入點來使用谊惭,以形成復合切入點。
5.5.3 聲明通知
Before
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
此處, dataAccessOperation
是在<aop:cofig>元素中定義的一個切入點的
id. 也可以通過
pointcut`屬性創(chuàng)建內(nèi)聯(lián)切入點:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
使用切入點名稱可以提高代碼的可讀性.
method
屬性指定了通知中的一個方法.
After Returning
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
returning="retVal" //定義返回值參數(shù)名稱
method="doAccessCheck"/>
...
</aop:aspect>
After Throwing
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="dataAccessEx" //定義具體的異常類型
method="doRecoveryActions"/>
...
</aop:aspect>
After Finally
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>
...
</aop:aspect>
Around
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut-ref="businessService"
method="doBasicProfiling"/>
...
</aop:aspect>
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
確定通知參數(shù)
使用arg-names
顯式指定參數(shù)名稱:
<aop:before
pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
method="audit"
arg-names="auditable"/>// 多個參數(shù)用逗號分隔
強類型參數(shù)示例:
public interface PersonService {
Person getPerson(String personName, int age);
}
public class DefaultFooService implements FooService {
public Person getPerson(String name, int age) {
return new Person(name, age);
}
}
通知:
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class SimpleProfiler {
// 強類型的參數(shù)
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}
xml配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="personService" class="x.y.service.DefaultPersonService"/>
<!-- this is the actual advice itself -->
<bean id="profiler" class="x.y.SimpleProfiler"/>
<aop:config>
<aop:aspect ref="profiler">
<aop:pointcut id="theExecutionOfSomePersonServiceMethod"
expression="execution(* x.y.service.PersonService.getPerson(String,int))
and args(name, age)"/>
<aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
method="profile"/>
</aop:aspect>
</aop:config>
</beans>
5.5.4 引入
聲明父類
在<aop:aspect>
元素中使用<aop:declare-parents>
元素聲明父類
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xzy.myapp.service.*+"
implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="com.xyz.myapp.SystemArchitecture.businessService()
and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
recordUsage方法:
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
5.5.5切面實例化模型
xml配置只支持單例模式.
5.5.6 Advisors
advisors
的概念來自于spring中AOP的定義, 在AspectJ
中沒有對等的定義. 一個advisor
就相當于擁有一個通知的自包含型切面.spring通過<aop:advisor>
元素來定義它, 最常見的是事務(wù)通知.
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
可以通過order
屬性來定義advisor的順序
5.6 選擇aop的定義樣式
如果在開發(fā)中確定要使用aspect, 是使用spring aop還是AspectJ, 是使用xml形式還是注解形式呢?
5.6.1 Spring AOP 與 Full AspectJ
如果你只是在spring的bean上執(zhí)行操作, 則用Spring AOP, 如果你還要操作一些不被spring容器管理的對象, 則用AspectJ.
5.6.2 @AspectJ 與 XML
在選擇了spring aop后, 怎么選擇是使用@AspectJ
還是xml
.
如果使用aop作為服務(wù)工具, 則使用xml更好(即切點表達式是否可能更改的配置的一部分), 而且使用xml配置, 可以更清楚的看到系統(tǒng)中哪些地方存在切面.
但xml有兩個缺點:
- 首先侮东,它沒有將它所處理的需求的實現(xiàn)完全封裝在一個地方圈盔。但是使用注解時, 這些都將被封裝在一個單模塊中.
- XML樣式在它能表達的內(nèi)容上稍微有些限制:只支持“單例”方面實例化模型,并且不可能組合XML中聲明的命名切入點悄雅。
@Pointcut("execution(* get*())")
public void propertyAccess() {}
@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}
@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}
在xml中,只能聲明前兩種切點:
<aop:pointcut id="propertyAccess"
expression="execution(* get*())"/>
<aop:pointcut id="operationReturningAnAccount"
expression="execution(org.xyz.Account+ *(..))"/>
xml的缺點是不能組合這兩種定義.
5.7 混合使用
兩種模式可以混合使用, 所有的這些實現(xiàn)都依賴相同的底層實現(xiàn)機制.
5.8 代理機制
Spring AOP使用JDK動態(tài)代理或CGLIB為給定的目標對象創(chuàng)建代理, 如果要代理的目標對象實現(xiàn)至少一個接口驱敲,則使用JDK動態(tài)代理。如果目標對象沒有實現(xiàn)任何接口宽闲,則創(chuàng)建一個CGLIB代理众眨。
如果想強制使用CGLIB代理, 則要考慮以下幾點:
- 不能通知
final
方法. 因為它不能被重寫. - 從Spring 4.0開始握牧,代理對象的構(gòu)造函數(shù)不再被調(diào)用兩次, since the CGLIB proxy instance is created through Objenesis. Only if your JVM does not allow for constructor bypassing, you might see double invocations and corresponding debug log entries from Spring’s AOP support.
在<aop:config>
元素上配置proxy-target-class
屬性為true即可.
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>
要在使用@AspectJ自動代理支持時強制CGLIB代理,請將<aop:aspectj-autoproxy>元素的代理目標類屬性設(shè)置為true.
<aop:aspectj-autoproxy proxy-target-class="true"/>
如果存在多個
<aop:config>
, 在運行時會將其合并成為一個<aop:config>
定義, 其中最強的代理配置將被使用.即在<tx:annotation-driven/>
或<aop:aspectj-autoproxy/>
或<aop:config/>
中任何一個上使用了proxy-target-class=true
, 都將強制以上三個都使用CGLIB代理.
5.8.1 理解AOP代理
spring aop是基于代理的.
首先考慮這樣一種場景:您有一個普通的娩梨、未代理的我碟、沒有任何特殊之處的、直接的對象引用姚建,如下面的代碼片段所示:
public class SimplePojo implements Pojo {
public void foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar();
}
public void bar() {
// some logic...
}
}
直接調(diào)用foo
方法, 方法將直接在對象引用上調(diào)用:
public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// this is a direct method call on the 'pojo' reference
pojo.foo();
}
}
當引用是一個代理時,則會發(fā)生變化:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
此處的關(guān)鍵是在main方法中對foo方法的調(diào)用.對該對象引用的方法調(diào)用是對代理的調(diào)用。因此吱殉,代理可以委托給與特定方法調(diào)用相關(guān)的所有攔截器(通知)掸冤。然而,一旦調(diào)用最終到達目標對象(在本例中是SimplePojo引用)友雳,它對自身執(zhí)行的任何方法調(diào)用稿湿,比如this.bar()或this.foo(),都將針對這個引用而不是代理來調(diào)用押赊。這具有重要意義饺藤。這意味著自調(diào)用不會導致與方法調(diào)用關(guān)聯(lián)的通知有機會執(zhí)行。
最好的方法是重構(gòu)代碼流礁,這樣就不會發(fā)生自調(diào)用涕俗。
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.adddInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
factory.setExposeProxy(true); ////////
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
最后,必須注意AspectJ沒有這個自調(diào)用問題神帅,因為它不是一個基于代理的AOP框架再姑。
5.9 @AspectJ代理的編程創(chuàng)建
除了通過使用<aop:config>
或<aop:aspectj-autoproxy>
在配置中聲明方面之外,還可以通過編程創(chuàng)建通知目標對象的代理. 在這里找御,我們只關(guān)注通過使用@AspectJ方面自動創(chuàng)建代理的能力元镀。
可以使用org.springframework.aop.aspectj.annotation。AspectJProxyFactory類來為一個或多個@AspectJ方面建議的目標對象創(chuàng)建代理霎桅。這個類的基本用法很簡單栖疑,如下例所示:
// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);
// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);
// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker);
// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();
5.10 在spring容器中使用AspectJ
如果您的需求超出了Spring AOP單獨提供的功能,我們將研究如何使用AspectJ編譯器或編織器來代替或補充Spring AOP滔驶。
要引入spring-aspects.jar.