第四章 面向切面的Spring
[TOC]
面向切面要解決的問題
在軟件開發(fā)中故响,散布于應(yīng)用中多處的功能被稱為 橫切關(guān)注點宏蛉,例如事務(wù)、安全庭砍、日志场晶、權(quán)限控制………通常來講,這些橫切關(guān)注點從概念上是與業(yè)務(wù)的應(yīng)用邏輯相分離的怠缸。把這些橫切關(guān)注點與業(yè)務(wù)邏輯相分離正是面向切面編程所要解決的問題诗轻。
橫切關(guān)注點可以被模塊化為特殊的類,這些類被稱為切面
面向切面常用術(shù)語
描述切面的常用術(shù)語有 :通知揭北、切點和連接點 概耻。
通知
在AOP術(shù)語中罐呼,切面(橫切關(guān)注點可以被模塊化為特殊的類)的工作被稱為通知鞠柄。通知定義了 切面是什么以及何時使用,除了描述切面所完成的工作嫉柴,通知還解決了何時執(zhí)行這個工作的問題厌杜。
Spring切面可以應(yīng)用五種類型的通知 :
- 前置通知 :在目標方法被 調(diào)用之前 調(diào)用通知功能
- 后置通知 :在目標方法 完成之后 調(diào)用通知,不會關(guān)心方法的輸出是什么
- 返回通知 :在目標方法 成功執(zhí)行 之后調(diào)用通知
- 異常通知 :在目標方法 拋出異常 后調(diào)用通知
- 環(huán)繞通知 :通知包裹了被通知的方法计螺,在目標方法 調(diào)用之前和調(diào)用之后 執(zhí)行自定義的行為
連接點
連接點就是在 應(yīng)用執(zhí)行過程中 能夠 插入切面 的一個點
切點
一個切面不需要通知應(yīng)用的所有連接點夯尽。切點有助于縮小切面所通知的連接點。如果說通知定義了切點是什么以及切點在何時使用的話登馒,那么切點就定義了“何處”匙握。切點的定義會匹配通知所要 織入 的一個或者多個連接點。
切面
切面是通知和切點的結(jié)合陈轿。通知和切點共同定義了切面的全部內(nèi)容——它是什么圈纺,在何時和何處完成其功能秦忿。
引入
引入允許我們向現(xiàn)有類添加新方法或者屬性。在無需修改現(xiàn)有的類的情況下蛾娶,讓他們具有新的行為和狀態(tài)灯谣。
織入
織入是把切面應(yīng)用到目標對象并創(chuàng)建新的代理的過程。切面在指定的連接點被織入到目標對象中蛔琅。在目標對象的生命周期里有多個點可以進行織入 :
- 編譯期 : 切面在目標類編譯時被織入胎许。這種方式需要有特殊的編譯器。
- 類加載期 :切面在目標類加載到JVM時被織入罗售。這種方式需要有特殊的類加載器辜窑。
- 運行期 :切面在應(yīng)用運行的某個某個時刻被織入。一般情況下寨躁,在織入切面時谬擦,AOP容器會為目標對象動態(tài)地創(chuàng)建一個代理對象。Spring AOP就是以這種方式注入切面的朽缎。
總結(jié)
通知包含了需要用于多個應(yīng)用對象的橫切行為(橫切關(guān)注點);連接點是程序執(zhí)行過程中能夠應(yīng)用通知的所有點谜悟;切點定義了通知被應(yīng)用的具體位置(在哪些連接點會得到通知)话肖。
Spring對AOP的支持
Spring提供了四種類型的AOP支持 :
- [x] 基于代理的經(jīng)典Spring AOP
- [ ] 純POJO切面(借助
aop
命名空間可以將POJO轉(zhuǎn)換為切面,需要XML配置) - [ ]
@AspectJ
注解驅(qū)動的切面(不使用XML來完成功能) - [ ] 注入式
AspectJ
切面(如果AOP需求超過了簡單的方法調(diào)用如構(gòu)造器或者屬性攔截葡幸,那么你需要考慮使用AspectJ
來實現(xiàn)切面最筒,適用于Spring各版本)
Spring通知是使用Java編寫的,定義通知所應(yīng)用的切點通常會使用注解或XML編寫蔚叨。
通過在代理類中包裹切面(通知 + 切點)床蜘,Spring在運行期把切面織入到Spring管理的bean中。如圖蔑水,代理類封裝了目標類邢锯,并攔截被通知方法的調(diào)用,再把調(diào)用轉(zhuǎn)發(fā)給真正的目標bean搀别。當代理攔截到方法調(diào)用時丹擎,在調(diào)用目標bean方法之前,會執(zhí)行切面邏輯歇父。
直到應(yīng)用需要被代理的bean時护戳,Spring才會創(chuàng)建代理對象。
因為Spring基于動態(tài)代理垂睬,所以Spring只支持方法連接點媳荒。Spring缺少對字段連接點的支持抗悍,無法讓我們創(chuàng)建細粒度的通知,例如攔截對象字段的修改肺樟。
通過切點來選擇連接點
使用AspectJ的切點表達式語言
在Spring AOP中檐春,使用AspectJ
的切點表達式語言定義切點,其中execution
是最重要的描述符 :
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除了返回類型么伯、方法名稱以及參數(shù)列表之外疟暖,其余都是可選的(即含有?
的都是可選的)
-
modifiers-pattern?
:方法修飾符(public、private田柔、默認俐巴、protected) -
ret-type-pattern
:方法返回類型 -
declaring-type-pattern?
:方法類所在路徑 -
name-pattern
:方法名 -
param-pattern
:方法參數(shù)類型,可有一個或者多個硬爆,用(..)
表示零個或者任意個參數(shù)欣舵,多個參數(shù)還可以用,
分隔,也可以用(*)
表示匹配任意類型的參數(shù) -
throws-pattern?
:表示方法拋出的異常
例如 :
匹配所有的public方法 :
execution(public * *(..))
匹配所有方法名開頭為set
的 :
execution(* set*(...))
匹配定義在AccountService
接口類中的所有方法
execution(* com.xyz.service.AccountService.*(...))
匹配定義在service
包下的所有方法
execution(* com.xyz.service.*.*(..))
匹配定義在service
包或者子包下的所有方法
execution(* com.xyz.service..*.*(..))
Aspect指示器 | 描述 |
---|---|
execution() |
用于匹配連接點 |
arg() |
表明連接點參數(shù)類型是匹配類 |
@args() |
表明參數(shù)注解是匹配類 |
this() |
匹配一個bean缀磕,這個bean是一個指定類型的實例 |
target |
匹配一個目標對象缘圈,此對象是一個給定類型的實例 |
@target() |
匹配對象類需要有指定類型的注解 |
within() |
限制連接點匹配指定的類型 |
@within() |
匹配方法,該方法需要給定一個特定注解 |
@annotation |
匹配帶有指定注解的連接點 |
編寫切點
假設(shè)我們需要編寫Performance
類型 :
public interface Performance{
public void perform();
}
我們想要編寫一個切面袜蚕,在調(diào)用Performance
類中的perform
方法時觸發(fā)通知 :
execution(public * concert.Performance.perform(..))
現(xiàn)在我們假設(shè)我們需要配置的切點僅僅匹配concert
包 :
execution(public * concert.Performance.perform(..) && within(concert.*))
當使用Spring的XML來描述切面時候糟把,我們可以使用and
來替換&&
,同樣的牲剃,or
和not
可以替換||
和!
在切點中選擇bean
Spring中的bean()
指示器允許我們在切點表達式中使用bean的ID來標識bean遣疯。bean()
使用bean ID
或bean
名稱作為參數(shù)來限制切點只匹配特定的bean
例如 :
execution(* concert.Performance.perform(..) and bean('woodstock'))
在上面的例子中,我們希望在執(zhí)行perform()
方法時應(yīng)用通知凿傅,但是限制bean的ID為woodstock
使用注解創(chuàng)建切面
定義切面
@Aspect
public class Audience {
// 演出之前
@Before("execution(public * concert.Performance.perform(..))")
public void silenceCellPhone(){
System.out.println("手機靜音");
}
// 演出之前
@Before("execution(public * concert.Performance.perform(..))")
public void takeSeats(){
System.out.println("對號入座");
}
// 演出成功之后
@AfterReturning("execution(public * concert.Performance.perform(..)")
public void applause(){
System.out.println("掌聲雷動");
}
// 演出失敗之后
@AfterThrowing("execution(public * concert.Performance.perform(..))")
public void demandRefund(){
System.out.println("我想退款!");
}
}
Audience
類使用了@Aspect
注解進行標注缠犀,表明該類不僅是一個POJO,還是一個切面聪舒。Audience
類中的方法都是用注解來定義切面的具體行為辨液。
AspectJ使用了五個注解來定義通知 :
注解 | 通知 |
---|---|
@After |
通知方法在目標方法返回或者拋出異常時調(diào)用 |
@AfterReturning |
通知方法在目標方法返回后調(diào)用 |
@AfterThrowing |
通知方法在目標方法拋出異常后調(diào)用 |
@Around |
通知方法會將目標方法封裝起來 |
@Before |
通知方法在目標方法調(diào)用之前調(diào)用 |
在上面的例子中,我們定義了四個切點表達式箱残,這四個表達式完全可以進行整合 :
PointCut
注解能夠在一個@Aspect
切面內(nèi)定義可以重復(fù)的切點
@Aspect
public class Audience {
@Pointcut("execution(public * concert.Performance.perform(..))")
public void perform(){}
// 演出之前
@Before("perform()")
public void silenceCellPhone(){
System.out.println("手機靜音");
}
// 演出之前
@Before("perform()")
public void takeSeats(){
System.out.println("對號入座");
}
// 演出成功之后
@AfterReturning("perform()")
public void applause(){
System.out.println("掌聲雷動");
}
// 演出失敗之后
@AfterThrowing("perform()")
public void demandRefund(){
System.out.println("我想退款!");
}
}
我們還需要啟動AspectJ的自動代理 :
如果你使用JavaConfig注解的話室梅,你可以在配置類上加上@EnableAspectJAutoProxy
注解啟動自動代理的功能
// 啟動AspectJ自動代理
@EnableAspectJAutoProxy
@Configuration
public class ConcertConfig {
@Bean
public Audience audience(){
return new Audience();
}
}
如果使用XML裝配的話,我們需要<aop:aspectj-autoproxy />
啟動自動代理 :
<bean id="audience" class="concert.Audience"/>
<bean id="musicPerformance" class="concert.MusicPerformance"/>
<!-- 開啟自動代理 -->
<aop:aspectj-autoproxy/>
Spring的AspectJ自動代理僅僅使用@Aspect
作為創(chuàng)建切面的指導(dǎo)疚宇,切面依然是基于代理的亡鼠。在本質(zhì)上,它依然是Spring基于代理的切面敷待。這一點非常重要间涵,因為這意味著盡管使用的是@Aspect
注解,但是仍然限于代理方法的調(diào)用榜揖。如果想使用AspectJ的所有能力勾哩,我們必須在運行時使用AspectJ并且不依賴Spring來創(chuàng)建基于代理的切面抗蠢。
創(chuàng)建環(huán)繞通知
環(huán)繞通知能夠讓你所編寫的邏輯將被通知的目標方法(連接點)完全包裹起來。就像是在一個通知方法中同時編寫前置后置通知思劳。
@Aspect
public class Audience {
@Pointcut("execution(public * concert.Performance.perform(..))")
public void perform(){}
// 環(huán)繞通知
@Around("perform()")
public void watchPerformance(ProceedingJoinPoint joinPoint){
try{
System.out.println("關(guān)閉手機");
System.out.println("入座");
// 通過ProceedingJoinPoint來調(diào)用被通知的方法
joinPoint.proceed();
System.out.println("掌聲雷動");
}catch(Throwable e){
System.out.println("我要退款");
}
}
}
@Around
注解表明watchPerformance()
方法會作為performance
切點的環(huán)繞通知迅矛。當通知方法需要把控制權(quán)交給被通知方法時候,需要調(diào)用ProceedingJoinPoint
的proceed()
方法潜叛。如果不調(diào)用這個方法的話秽褒,你的通知會阻塞對被通知方法的調(diào)用。
為通知傳遞參數(shù)
在BlankDisc
中威兜,我們需要統(tǒng)計磁道被播放的數(shù)量 :
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
private List<String> tracks;
public BlankDisc(String title,String artist,List<String> tracks) {
this.title = title;
this.artist = artist;
this.tracks = tracks;
}
public String getTitle() {
return title;
}
public String getArtist() {
return artist;
}
public List<String> getTracks() {
return tracks;
}
public void play() {
System.out.println("title :" + title + " artist :" + artist);
for (int trackNumber = 0;trackNumber < tracks.size();trackNumber ++){
playTrack(trackNumber);
}
}
public void playTrack(int trackNumber) {
System.out.println("track "+ trackNumber + " : " + tracks.get(trackNumber));
}
}
我們定義TrackCounter
來描述切面 :
@Aspect
@Component
@EnableAspectJAutoProxy
public class TrackCounter {
public Map<Integer,Integer> trackCounts = new HashMap<Integer, Integer>();
@Pointcut("execution(public * soundsystem.BlankDisc.playTrack(int)) && args(trackNumber))")
public void trackPlayed(int trackNumber){}
@AfterReturning("trackPlayed(trackNumber)")
public void countTrack(int trackNumber){
trackCounts.put(trackNumber,getPlayCount(trackNumber) + 1);
System.out.println("--->track " + trackNumber + "數(shù)量增加了.");
}
public int getPlayCount(int trackNumber){
return trackCounts.containsKey(trackNumber)?
trackCounts.get(trackNumber):0;
}
}
切點表達式中的args(trackNumber)
表明 :傳遞給連接點的int類型的參數(shù)也會傳遞到通知方法中销斟。參數(shù)的名稱為trackNumber
,與切點方法簽名中的參數(shù)相匹配椒舵。在@AfterReturing("trackNumber")
表達式下面蚂踊,切點方法和切點定義的參數(shù)名一致。
通過注解引入新功能
TODO
在XML中聲明切面
前置后置通知
<!-- 切面配置 -->
<!-- 頂層的aop配置元素 -->
<aop:config>
<!-- 定制一個切面 -->
<aop:aspect ref="audience">
<!-- 定義一個切點 -->
<aop:pointcut id="perform" expression="execution(public * concert.Performance.perform(..))"/>
<aop:before method="takeSeats" pointcut-ref="perform"/>
<aop:before method="silenceCellPhone" pointcut-ref="perform"/>
<aop:after-returning method="applause" pointcut-ref="perform"/>
<aop:after-throwing method="demandRefund" pointcut-ref="perform"/>
</aop:aspect>
</aop:config>
環(huán)繞通知
<bean id="musicPerformance" class="concert.MusicPerformance"/>
<bean id="audience" class="concert.Audience"/>
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="performance" expression="execution(public * concert.Performance.perform())"/>
<aop:around method="execute" pointcut-ref="performance"/>
</aop:aspect>
</aop:config>
為通知傳遞參數(shù)
<beans>
<bean id="blankDisc" class="soundsystem.BlankDisc"
c:_0="${disc.title}" c:_1="${disc.artist}" c:_2-ref="blankDiscList"/>
<bean id="cdPlayer" class="soundsystem.CDPlayer"/>
<bean id="trackCounter" class="soundsystem.TrackCounter"/>
<util:list id="blankDiscList">
<value>老古董</value>
<value>大千世界</value>
<value>如約而至</value>
<value>柳成蔭</value>
</util:list>
<context:property-placeholder location="classpath:/application.properties"/>
<aop:aspectj-autoproxy/>
<import resource="classpath:/aopconfig.xml"/>
</beans>
<beans>
<aop:config>
<aop:aspect ref="trackCounter">
<aop:pointcut id="playTrack" expression="execution(* soundsystem.BlankDisc.playTrack(int)) and args(trackNumber)"/>
<aop:after-returning pointcut-ref="playTrack" method="countTrack"/>
</aop:aspect>
</aop:config>
</beans>
通過切面引入新的功能
TODO
注入AspectJ切面
TODO