本章內(nèi)容:
- 面向切面編程的基本原理
- 通過(guò)POJO創(chuàng)建切面
- 使用@AspectJ注解
- 為AspectJ切面注入依賴
軟件系統(tǒng)中的一些功能需要用到應(yīng)用程序的多個(gè)地方,但是我們又不想在每個(gè)點(diǎn)都明確調(diào)用它們。日志、安全和事務(wù)管理的確都很重要,但它們不應(yīng)是應(yīng)用對(duì)象主動(dòng)參與的行為,應(yīng)該要讓應(yīng)用對(duì)象只關(guān)注于自己所針對(duì)的業(yè)務(wù)領(lǐng)域問題,其他方面的問題由其他應(yīng)用對(duì)象來(lái)處理。
在軟件開發(fā)中,散布于應(yīng)用中多處的功能被稱為橫切關(guān)注點(diǎn)(cross-cutting concern)。通常,這些橫切關(guān)注點(diǎn)從概念上與應(yīng)用的業(yè)務(wù)邏輯分離的(但是往往會(huì)直接嵌入到應(yīng)用的業(yè)務(wù)邏輯之中)。把這些橫切關(guān)注點(diǎn)與業(yè)務(wù)邏輯相分離正是面向切面編程(AOP)所要解決的問題。
裝配Bean介紹了如何使用依賴注入(DI)管理和配置我們的應(yīng)用對(duì)象。DI有助于應(yīng)用對(duì)象之間的解耦,而AOP可以實(shí)現(xiàn)橫切關(guān)注點(diǎn)與它們所影響的對(duì)象之間的解耦。
本章展示了Spring對(duì)切面的支持,包括如何把普通類聲明為一個(gè)切面和如何使用注解創(chuàng)建切面。除此之外,還會(huì)看到AspectJ(另一種流行的AOP實(shí)現(xiàn))如何補(bǔ)充AOP框架的功能。
什么是面向切面編程
切面能幫助我們模塊化橫切關(guān)注點(diǎn)。橫切關(guān)注點(diǎn)可以被描述為影響應(yīng)用多處的功能。下圖呈現(xiàn)了橫切關(guān)注點(diǎn)的概念:
上圖展現(xiàn)了一個(gè)被劃分為模塊的典型應(yīng)用。每個(gè)模塊的核心功能都是為特定業(yè)務(wù)領(lǐng)域提供服務(wù),但是這些模塊都需要類似的輔助功能。
重用通用功能最常見的面向?qū)ο蠹夹g(shù)是繼承(inheritance)或委托(delegation)。但是,如果在整個(gè)應(yīng)用中都使用相同的基類,繼承往往會(huì)導(dǎo)致一個(gè)脆弱的對(duì)象體系;而使用委托可能需要對(duì)委托對(duì)象進(jìn)行復(fù)雜的調(diào)用。
切面提供了取代繼承和委托的另一種可選方案,而且在很多場(chǎng)景下更清晰簡(jiǎn)潔。在使用面向切面編程時(shí),仍然在一個(gè)地方定義通用功能,但是可以通過(guò)聲明的方式定義這個(gè)功能要以何種方式在何處應(yīng)用,不需要修改受影響的類。橫切關(guān)注點(diǎn)可以被模塊化為特殊的類,這些類被稱為切面(aspect)。
這樣做有兩個(gè)好處:
- 現(xiàn)在每個(gè)關(guān)注點(diǎn)都集中于一個(gè)地方,而不是分散到多處代碼中;
- 服務(wù)模塊更簡(jiǎn)潔,因?yàn)樗鼈冎话饕P(guān)注點(diǎn)(或核心功能)的代碼,而次要關(guān)注點(diǎn)的代碼被轉(zhuǎn)移到切面中了。
定義AOP術(shù)語(yǔ)
描述切面的常用術(shù)語(yǔ)有通知(advice)、切點(diǎn)(pointcut)和連接點(diǎn)(join point)。下圖展示了這些概念是如何關(guān)聯(lián)在一起的:
通知(Advice)
在AOP術(shù)語(yǔ)中,切面要完成的工作被稱為通知。
通知定義了切面是什么以及何時(shí)使用。除了描述切面要完成的工作,通知還解決了何時(shí)執(zhí)行這個(gè)工作的問題。
Spring切面可以應(yīng)用5種類型的通知:
- 前置通知(Before):在目標(biāo)方法被調(diào)用之前調(diào)用通知功能;
- 后置通知(After):在目標(biāo)方法完成之后調(diào)用通知,此時(shí)不會(huì)關(guān)心方法的輸出是什么;
- 返回通知(After-returning):在目標(biāo)方法成功執(zhí)行之后調(diào)用通知;
- 異常通知(After-throwing):在目標(biāo)方法拋出異常后調(diào)用通知;
- 環(huán)繞通知(Around):通知包裹了被通知的方法,在被通知的方法調(diào)用之前和調(diào)用之后執(zhí)行自定義的行為。
連接點(diǎn)(Join point)
應(yīng)用可能有數(shù)以千計(jì)的時(shí)機(jī)應(yīng)用通知。這些時(shí)機(jī)被稱為連接點(diǎn)。連接點(diǎn)是在應(yīng)用執(zhí)行過(guò)程中能夠插入切面的一個(gè)點(diǎn)。切面代碼可以利用這些點(diǎn)插入到應(yīng)用的正常流程之中,并添加新的行為。
切點(diǎn)(Poincut)
一個(gè)切面并不需要通知應(yīng)用的所有連接點(diǎn)。切點(diǎn)有助于縮小切面所通知的連接點(diǎn)的范圍。
如果說(shuō)通知定義了切面的“什么”和“何時(shí)”的話,那么切點(diǎn)就定義了“何處”。切點(diǎn)的定義會(huì)匹配通知所要織入的一個(gè)或多個(gè)連接點(diǎn)。通常使用明確的類和方法名稱,或是利用正則表達(dá)式定義所匹配的類和方法名稱來(lái)指定這些切點(diǎn)。有些AOP框架允許創(chuàng)建動(dòng)態(tài)的切點(diǎn),可以根據(jù)運(yùn)行時(shí)的決策來(lái)決定是否應(yīng)用通知。
切面(Aspect)
切面是通知和切點(diǎn)的結(jié)合。通知和切點(diǎn)共同定義了切面的全部?jī)?nèi)容——它是什么,在何時(shí)和何處完成其功能。
引入(Introduction)
引入允許向現(xiàn)有的類添加新方法或?qū)傩浴亩梢栽跓o(wú)需修改這些現(xiàn)有的類的情況下,讓它們具有新的行為和狀態(tài)。
織入(Weaving)
織入是把切面應(yīng)用到目標(biāo)對(duì)象并創(chuàng)建新的代理對(duì)象的過(guò)程。切面在指定的連接點(diǎn)被織入到目標(biāo)對(duì)象中。在目標(biāo)對(duì)象的生命周期里有多個(gè)點(diǎn)可以進(jìn)行織入:
- 編譯期:切面在目標(biāo)類編譯時(shí)被織入。這種方式需要特殊的編譯器。AspectJ的織入編譯器就是以這種方式織入切面的。
- 類加載期:切面在目標(biāo)類加載到JVM時(shí)被織入。這種方式需要特殊的類加載器(ClassLoader),它可以在目標(biāo)類被引入應(yīng)用之前增強(qiáng)該目標(biāo)類的字節(jié)碼。AspectJ 5的加載時(shí)織入(load-time weaving,LTW)就支持以這種方式織入切面。
- 運(yùn)行期:切面在應(yīng)用運(yùn)行的某個(gè)時(shí)刻被織入。一般情況下,在織入切面時(shí),AOP容器會(huì)為目標(biāo)對(duì)象動(dòng)態(tài)地創(chuàng)建一個(gè)代理對(duì)象。Spring AOP就是以這種方式織入切面的。
Spring對(duì)AOP的支持
Spring提供了4種類型的AOP支持:
- 基于代理的經(jīng)典Spring AOP;
- 純POJO切面;
- @AspectJ注解驅(qū)動(dòng)的切面;
- 注入式AspectJ切面(適用于Spring各版本)。
前三種都是Spring AOP實(shí)現(xiàn)的變體,Spring AOP構(gòu)建在動(dòng)態(tài)代理基礎(chǔ)之上,因此,Spring對(duì)AOP的支持局限于方法攔截。
Spring的經(jīng)典AOP編程模型曾經(jīng)的確很棒,但是現(xiàn)在Spring提供了更簡(jiǎn)潔和干凈的面向切面編程方式。。引入了簡(jiǎn)單的聲明式AOP和基于注解的AOP之后,Spring經(jīng)典的AOP看起來(lái)就顯得非常笨重和過(guò)于復(fù)雜,直接使用ProxyFactory Bean會(huì)讓人感覺厭煩。
借助Spring的aop命名空間,可以將純POJO轉(zhuǎn)換為切面。實(shí)際上這些POJO只是提供了滿足切點(diǎn)條件時(shí)所要調(diào)用的方法。這種技術(shù)需要XML配置。
Spring借鑒了AspectJ的切面,以提供注解驅(qū)動(dòng)的AOP。本質(zhì)上依然是Spring基于代理的AOP,但是編程模型幾乎與編寫成熟的AspectJ注解切面完全一致。這種AOP風(fēng)格的好處在于能夠不使用XML來(lái)完成功能。
如果AOP需求超過(guò)了簡(jiǎn)單的方法調(diào)用,那么需要考慮使用AspectJ來(lái)實(shí)現(xiàn)切面。注入式AspectJ切面能夠幫助你將值注入到AspectJ驅(qū)動(dòng)的切面中。
開始學(xué)習(xí)Spring AOP技術(shù)之前,必須要了解Spring AOP框架的一些關(guān)鍵知識(shí)。
Spring通知是Java編寫的
Spring所創(chuàng)建的通知都是用標(biāo)準(zhǔn)的Java類編寫的。這樣就可以使用與普通Java開發(fā)一樣的集成開發(fā)環(huán)境(IDE)來(lái)開發(fā)切面。定義通知所應(yīng)用的切點(diǎn)通常會(huì)使用注解或在Spring配置文件里采用XML來(lái)編寫。
AspectJ與之相反。AspectJ最初是以Java語(yǔ)言擴(kuò)展的方式實(shí)現(xiàn)的。
Spring在運(yùn)行時(shí)通知對(duì)象
通過(guò)在代理類中包裹切面,Spring在運(yùn)行期把切面織入到Spring管理的bean中。如下圖所示:
代理類封裝了目標(biāo)類,并攔截被通知方法的調(diào)用,再把調(diào)用轉(zhuǎn)發(fā)給真正的目標(biāo)bean。當(dāng)代理攔截到方法調(diào)用時(shí),在調(diào)用目標(biāo)bean方法之前,會(huì)執(zhí)行切面邏輯。
直到應(yīng)用需要被代理的bean時(shí),Spring才創(chuàng)建代理對(duì)象。如果使用的是ApplicationContext的話,在ApplicationContext從BeanFactory中加載所有bean的時(shí)候,Spring才會(huì)創(chuàng)建被代理的對(duì)象。
Spring只支持方法級(jí)別的連接點(diǎn)
通過(guò)使用各種AOP方案可以支持多種連接點(diǎn)模型。因?yàn)镾pring基于動(dòng)態(tài)代理,所以Spring只支持方法連接點(diǎn)。Spring缺少對(duì)字段連接點(diǎn)的支持,無(wú)法讓我們創(chuàng)建細(xì)粒度的通知,例如攔截對(duì)象字段的修改。而且它不支持構(gòu)造器連接點(diǎn),我們就無(wú)法在bean創(chuàng)建時(shí)應(yīng)用通知。
方法攔截可以滿足絕大部分的需求。其他連接點(diǎn)攔截功能可以利用Aspect來(lái)補(bǔ)充Spring AOP的功能。
通過(guò)切點(diǎn)來(lái)選擇連接點(diǎn)
切點(diǎn)用于準(zhǔn)確定位應(yīng)該在什么地方應(yīng)用切面的通知。
在Spring AOP中,要使用AspectJ的切點(diǎn)表達(dá)式語(yǔ)言來(lái)定義切點(diǎn)。
是Spring僅支持AspectJ切點(diǎn)指示器(pointcut designator)的一個(gè)子集。因?yàn)镾pring是基于代理的,而某些切點(diǎn)表達(dá)式是與基于代理的AOP無(wú)關(guān)的。
下表列出了Spring AOP所支持的AspectJ切點(diǎn)指示器:
嘗試使用AspectJ其他指示器時(shí),將會(huì)拋出IllegalArgument-Exception異常。
這些Spring支持的指示器,只有execution指示器是實(shí)際執(zhí)行匹配的,而其他的指示器都是用來(lái)限制匹配的。
編寫切面
定義一個(gè)Performance接口:
package concert;
public interface Performance{
public void perform();
}
Performance可以代表任何類型的現(xiàn)場(chǎng)表演。假設(shè)想編寫Performance的perform()方法觸發(fā)的通知。下圖展現(xiàn)了一個(gè)切點(diǎn)表達(dá)式,這個(gè)表達(dá)式設(shè)置當(dāng)perform()方法執(zhí)行時(shí)觸發(fā)通知的調(diào)用。
我們使用execution()指示器選擇Performance的perform()方法。方法表達(dá)式以“*”號(hào)開始,表明不關(guān)心方法返回值的類型。然后,指定了全限定類名和方法名。對(duì)于方法參數(shù)列表,使用兩個(gè)點(diǎn)號(hào)(..)表明切點(diǎn)選擇任意的perform()方法,無(wú)論該方法的入?yún)⑹鞘裁础?/p>
假設(shè)需要配置的切點(diǎn)僅匹配concert包。可以使用within()指示器來(lái)限制匹配:
注意使用了“&&”操作符把execution()和within()指示器連接在一起形成與(and)關(guān)系。類似地,可以使用“||”操作符來(lái)標(biāo)識(shí)或(or)關(guān)系,使用“!”操作符來(lái)標(biāo)識(shí)非(not)操作。
由于“&”在XML中有特殊含義,在Spring的XML配置里面描述切點(diǎn)時(shí),可以使用and來(lái)代替“&&”。同樣,or和not可以分別用來(lái)代替“||”和“!”。
在切點(diǎn)中選擇bean
除了上表所示的AspectJ指示器,Spring還引入了一個(gè)新的bean()指示器,它允許我們?cè)谇悬c(diǎn)表達(dá)式中使用bean的ID來(lái)標(biāo)識(shí)bean。bean()使用Bean ID或bean名稱作為參數(shù)來(lái)限制切點(diǎn)只匹配特定的bean。
例如:
excution(* concert.Performance.perform())
and bean('woodstock')
在執(zhí)行Performance的perform()方法時(shí)應(yīng)用通知,限定的bean的ID為woodstock。
還可以使用非操作除了特定ID以外的其他bean應(yīng)用通知:
excution(* concert.Performance.perform())
and !bean('woodstock')
切面的通知會(huì)被編織到所有ID不為woodstock的bean中。
使用注解創(chuàng)建切面
使用注解來(lái)創(chuàng)建切面是AspectJ 5所引入的關(guān)鍵特性。AspectJ 5之前,編寫AspectJ切面需要學(xué)習(xí)一種Java語(yǔ)言的擴(kuò)展,AspectJ面向注解的模型可以非常簡(jiǎn)便地通過(guò)少量注解把任意類轉(zhuǎn)變?yōu)榍忻妗?/p>
定義切面
從演出的角度來(lái)看,觀眾非常重要,但是對(duì)演出本身的功能來(lái)講,它并不是核心,這是一個(gè)單獨(dú)的關(guān)注點(diǎn)。因此,將觀眾定義為一個(gè)切面,并將其應(yīng)用到演出上就是較為明智的做法。
定義一個(gè)Audience類,它定義了我們所需的一個(gè)切面:
Audience類使用@AspectJ注解進(jìn)行了標(biāo)注。該注解表明Audience是一個(gè)切面。Audience類中的方法都使用注解來(lái)定義切面的具體行為。
Audience有四個(gè)方法,定義了一個(gè)觀眾在觀看演出時(shí)可能會(huì)做的事情。這些方法都使用了通知注解來(lái)表明它們應(yīng)該在什么時(shí)候調(diào)用。AspectJ提供了五個(gè)注解來(lái)定義通知,如下表所示:
Audience使用到了前面五個(gè)注解中的三個(gè)。takeSeats()
和silenceCellPhones()
方法都用到了@Before
注解,表明它們應(yīng)該在演出開始之前調(diào)用。applause()
方法使用了@AfterReturning
注解,它會(huì)在演出成功返回后調(diào)用。demandRefund()
方法上添加了@AfterThrowing
注解,這表明它會(huì)在拋出異常以后執(zhí)行。
代碼中所有的這些注解都給定了一個(gè)切點(diǎn)表達(dá)式作為它的值,這四個(gè)方法的切點(diǎn)表達(dá)式都是相同的。如果我們只定義這個(gè)切點(diǎn)一次,然后每次需要的時(shí)候引用它,效果可能會(huì)更好。
@Pointcut注解能夠在一個(gè)@AspectJ切面內(nèi)定義可重用的切點(diǎn):
在Audience中,performance()方法使用了@Pointcut注解。為@Pointcut注解設(shè)置的值是一個(gè)切點(diǎn)表達(dá)式。通過(guò)在performance()方法上添加@Pointcut注解,擴(kuò)展了切點(diǎn)表達(dá)式語(yǔ)言,這樣就可以在任何的切點(diǎn)表達(dá)式中使用performance()。
performance()方法的實(shí)際內(nèi)容并不重要,該方法本身只是一個(gè)標(biāo)識(shí),供@Pointcut注解依附。
需要注意的是,除了注解和沒有實(shí)際操作的performance()方法,Audience類依然是一個(gè)POJO。能夠像使用其他的Java類那樣調(diào)用它的方法,它的方法也能夠獨(dú)立地進(jìn)行單元測(cè)試。
像其他的Java類一樣,它可以裝配為Spring中的bean:
@Bean
public Audience audience() {
return new Audience();
}
但是目前Audience仍然只是Spring容器中的一個(gè)bean。即便使用了AspectJ注解,也并不會(huì)被視為切面,這些注解不會(huì)解,也不會(huì)創(chuàng)建將其轉(zhuǎn)換為切面的代理。
Java Config啟用AspectJ注解的自動(dòng)代理
暫時(shí)略過(guò)
XML啟用AspectJ自動(dòng)代理
要使用XML來(lái)裝配bean的話,那么需要使用Spring aop命名空間中的<aop:aspectj-autoproxy>元素:
AspectJ自動(dòng)代理會(huì)為使用@Aspect注解的bean創(chuàng)建一個(gè)代理,這個(gè)代理會(huì)圍繞著所有該切面的切點(diǎn)所匹配的bean。
,Spring的AspectJ自動(dòng)代理僅僅使用@Aspect作為創(chuàng)建切面的指導(dǎo),切面依然是基于代理的。本質(zhì)上依然是Spring基于代理的切面。
創(chuàng)建環(huán)繞通知
環(huán)繞通知是最為強(qiáng)大的通知類型。它能夠讓你所編寫的邏輯將 被通知的目標(biāo)方法完全包裝起來(lái)。就像在一個(gè)通知方法中同時(shí)編寫前置通知和后置通知。
重新實(shí)現(xiàn)Audience切面:
@Around注解表明watchPerformance()方法會(huì)作為performance()切點(diǎn)的環(huán)繞通知。在這個(gè)通知中,觀眾在演出之前會(huì)將手機(jī)調(diào)至靜音并就坐,演出結(jié)束后會(huì)鼓掌喝彩。如果演出失敗的話,觀眾會(huì)要求退款。
新的通知方法接受ProceedingJoinPoint作為參數(shù),這個(gè)對(duì)象是必要的,因?yàn)橐谕ㄖ型ㄟ^(guò)它來(lái)調(diào)用被通知的方法。通知方法中當(dāng)要將控制權(quán)交給被通知的方法時(shí),調(diào)用ProceedingJoinPoint的proceed()
方法。
如果不調(diào)proceed()方法,通知實(shí)際上會(huì)阻塞對(duì)被通知方法的調(diào)用。
處理通知中的參數(shù)
目前為止編寫的切面都很簡(jiǎn)單,沒有任何參數(shù)。如果切面所通知的方法確實(shí)有參數(shù)該怎么辦呢?切面怎么訪問和使用傳遞給被通知方法的參數(shù)?
重新看一下裝配Bean章節(jié)的BlankDisc樣例:
package soundsystem.properties;
import java.util.List;
import soundsystem.CompactDisc;
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
private List<String> tracks;
public void setTitle(String title) {
this.title = title;
}
public void setArtist(String artist) {
this.artist = artist;
}
public void setTracks(List<String> tracks) {
this.tracks = tracks;
}
public void play() {
System.out.println("Playing " + title + " by " + artist);
for (String track : tracks) {
System.out.println("-Track: " + track);
}
}
}
play()方法會(huì)循環(huán)所有的磁道并調(diào)用playTrack()方法。但是,我們也可以創(chuàng)建一個(gè)playTrack()方法直接播放某一個(gè)磁道中的歌曲。
假設(shè)想記錄每個(gè)磁道被播放的次數(shù)。一種方法就是修改playTrack()方法,直接在每次調(diào)用的時(shí)候記錄這個(gè)數(shù)量。但是,記錄磁道的播放次數(shù)與播放本身是不同的關(guān)注點(diǎn),因此不應(yīng)該屬于playTrack()方法。這應(yīng)該是切面要完成的任務(wù)。
為了記錄每個(gè)磁道所播放的次數(shù),創(chuàng)建了TrackCounter類,它是是通知playTrack()方法的一個(gè)切面:
切面使用@Pointcut注解定義命名的切點(diǎn),并使用@Before將一個(gè)方法聲明為前置通知。這里的不同點(diǎn)在于切點(diǎn)還聲明了要提供給通知方法的參數(shù)。
需要關(guān)注的是切點(diǎn)表達(dá)式中的args(trackNumber)限定符。它表明傳遞給playTrack()方法的int類型參數(shù)也會(huì)傳遞到通知中去。參數(shù)的名稱trackNumber也與切點(diǎn)方法簽名中的參數(shù)相匹配。
這個(gè)參數(shù)會(huì)傳遞到通知方法中,這個(gè)通知方法通過(guò)@Before注解和命名切點(diǎn)trackPlayed(trackNumber)定義的。切點(diǎn)定義中的參數(shù)與切點(diǎn)方法中的參數(shù)名稱是一樣的,這樣就實(shí)現(xiàn)了從命名切點(diǎn)到通知方法的參數(shù)轉(zhuǎn)移。
現(xiàn)在,可以在Spring配置中將BlankDisc和TrackCounter定義為bean,并啟用AspectJ自動(dòng)代理:
編寫一個(gè)簡(jiǎn)單測(cè)試。播放幾個(gè)磁道并通過(guò)TrackCounter斷言播放的數(shù)量。
目前為止,所使用的切面中,所包裝的都是被通知對(duì)象的已有方法。方法包裝僅僅是切面所能實(shí)現(xiàn)的功能之一。接下來(lái)看如何通過(guò)編寫切面,為被通知的對(duì)象引入全新的功能。
通過(guò)注解引入新功能
我們還沒有為對(duì)象增加任何新的方法,但是已經(jīng)為對(duì)象擁有的方法添加了新功能。如果切面能夠?yàn)楝F(xiàn)有的方法增加額外的功能,為什么不能為一個(gè)對(duì)象增加新的方法呢?利用被稱為引入的AOP概念,切面可以為Spring bean添加新方法。
在Spring中,切面只是實(shí)現(xiàn)了它們所包裝bean相同接口的代理。如果除了實(shí)現(xiàn)這些接口,代理也能暴露新接口的話,切面所通知的bean看起來(lái)像是實(shí)現(xiàn)了新的接口,即便底層實(shí)現(xiàn)類并沒有實(shí)現(xiàn)這些接口也無(wú)所謂。
當(dāng)引入接口的方法被調(diào)用時(shí),代理會(huì)把此調(diào)用委托給實(shí)現(xiàn)了新接口的某個(gè)其他對(duì)象。實(shí)際上,一個(gè)bean的實(shí)現(xiàn)被拆分到了多個(gè)類中。
為了驗(yàn)證這個(gè)思路,為示例中的所有的Performance實(shí)現(xiàn)引入下面的Encoreable接口:
package concert;
public interface Encoreable {
void performEncore();
}
需要有一種方式將這個(gè)接口應(yīng)用到Performance實(shí)現(xiàn)中。現(xiàn)在假設(shè)能夠訪問Performance的所有實(shí)現(xiàn),并對(duì)其進(jìn)行修改,讓它們都實(shí)現(xiàn)Encoreable接口。從設(shè)計(jì)的角度來(lái)看,這并不是最好的做法,并不是所有的Performance都是具有Encoreable特性的。另外一方面,有可能無(wú)法修改所有的Performance實(shí)現(xiàn)。
借助于AOP的引入功能,我們可以不必在設(shè)計(jì)上妥協(xié)或者侵入性地改變現(xiàn)有的實(shí)現(xiàn)。為了實(shí)現(xiàn)該功能,創(chuàng)建一個(gè)新的切面:
EncoreableIntroducer是一個(gè)切面。它與之前所創(chuàng)建的切面不同,它并沒有提供前置、后置或環(huán)繞通知,而是通過(guò)@DeclareParents
注解,將Encoreable接口引入到Performance bean中。
@DeclareParents注解由三部分組成:
- value屬性指定了哪種類型的bean要引入該接口。在本例中,是所有實(shí)現(xiàn)Performance的類型。(標(biāo)記符后面的加號(hào)表示是Performance的所有子類型,而不是Performance本身。)
- defaultImpl屬性指定了為引入功能提供實(shí)現(xiàn)的類。在這里指定的是DefaultEncoreable提供實(shí)現(xiàn)。
- @DeclareParents注解所標(biāo)注的靜態(tài)屬性指明了要引入了接口。在這里,我們所引入的是Encoreable接口。
和其他的切面一樣,我們需要在Spring應(yīng)用中將EncoreableIntroducer聲明為一個(gè)bean:
<bean class="concert.EncoreableIntroducer" />
Spring的自動(dòng)代理機(jī)制將會(huì)獲取到它的聲明,當(dāng)Spring發(fā)現(xiàn)一個(gè)bean使用了@Aspect注解時(shí),Spring就會(huì)創(chuàng)建一個(gè)代理,然后將調(diào)用委托給被代理的bean或被引入的實(shí)現(xiàn),這取決于調(diào)用的方法屬于被代理的bean還是屬于被引入的接口。
Spring注解和自動(dòng)代理提供了一種很便利的方式來(lái)創(chuàng)建切面。但是面向注解的切面聲明有一個(gè)明顯的劣勢(shì):必須能夠?yàn)橥ㄖ愄砑幼⒔狻榱俗龅竭@一點(diǎn),必須要有源碼。
如果沒有源碼的話,或者不想將AspectJ注解放到代碼之中,Spring為切面提供了另外一種可選方案。
在XML中聲明切面
如果需要聲明切面,但是又不能為通知類添加注解的時(shí)候,就必須轉(zhuǎn)向XML配置。
在Spring的aop命名空間中,提供了多個(gè)元素用來(lái)在XML中聲明切面。
aop命名空間的其他元素能夠直接在Spring配置中聲明切面,而不需要使用注解。
現(xiàn)在將Audience類的所有AspectJ注解全部移除掉:
盡管看起來(lái)并沒有什么差別,但Audience已經(jīng)具備了成為AOP通知的所有條件。
聲明前置和后置通知
使用Spring aop命名空間中的一些元素,將沒有注解的Audience類轉(zhuǎn)換為切面:
在<aop:config>元素內(nèi),可以聲明一個(gè)或多個(gè)通知器、切面或者切點(diǎn)。在上面的例子中,使用<aop:aspect>元素聲明了一個(gè)簡(jiǎn)單的切面。ref元素引用了一個(gè)POJO bean,該bean實(shí)現(xiàn)了切面的功能。ref元素所引用的bean提供了在切面中通知所調(diào)用的方法。
第一個(gè)需要注意的事項(xiàng)是大多數(shù)的AOP配置元素必須在<aop:config>元素的上下文內(nèi)使用。這條規(guī)則有幾種例外場(chǎng)景,但是把bean聲明為一個(gè)切面時(shí),總是從<aop:config>元素開始配置的。
示例切面應(yīng)用了四個(gè)不同的通知。兩個(gè)<aop:before>元素定義了匹配切點(diǎn)的方法執(zhí)行之前調(diào)用前置通知方法(由method屬性聲明)。<aop:after-returning>元素定義了一個(gè)返回(after-returning)通知,在切點(diǎn)所匹配的方法調(diào)用之后再調(diào)用后置通知方法。<aop:after-throwing>元素定義了異常(after-throwing)通知,如果所匹配的方法執(zhí)行時(shí)拋出任何的異常,都將調(diào)用demandRefund()方法。下圖展示了通知邏輯如何織入到業(yè)務(wù)邏輯中:
在所有的通知元素中,pointcut屬性定義了通知所應(yīng)用的切點(diǎn),它的值是使用AspectJ切點(diǎn)表達(dá)式語(yǔ)法所定義的切點(diǎn)。
在基于AspectJ注解的通知中,當(dāng)發(fā)現(xiàn)這種類型的重復(fù)時(shí),我們使用@Pointcut注解消除了這些重復(fù)的內(nèi)容。在基于XML的切面聲明中,需要使用<aop:pointcut>元素。
如下的XML展現(xiàn)了如何將通用的切點(diǎn)表達(dá)式抽取到一個(gè)切點(diǎn)聲明中,,這樣這個(gè)聲明就能在所有的通知元素中使用了。
現(xiàn)在切點(diǎn)在一個(gè)地方定義的,并被多個(gè)通知元素引用。<aop:pointcut>
元素定義了一個(gè)id為performance的切點(diǎn)。同時(shí)修改所有的通知元素,用 pointcut-ref
屬性來(lái)引用這個(gè)命名切點(diǎn)。
聲明環(huán)繞通知
前置通知和后置通知有一些限制。如果不使用成員變量存儲(chǔ)信息,在前置通知和后置通知之間共享信息會(huì)非常麻煩。
希望并報(bào)告每個(gè)節(jié)目表演了多長(zhǎng)時(shí)間。使用前置通知和后置通知實(shí)現(xiàn)該功能的唯一方式是在前置通知中記錄開始時(shí)間并在某個(gè)后置通知中報(bào)告表演耗費(fèi)的時(shí)間。這樣的話我們必須在一個(gè)成員變量中保存開始時(shí)間。因?yàn)锳udience是單例的,如果像這樣保存狀態(tài)的話,將會(huì)存在線程安全問題。
環(huán)繞通知相比于前置通知和后置通知在這點(diǎn)上有明顯的優(yōu)勢(shì),使用環(huán)繞通知,我們可以完成前置通知和后置通知所實(shí)現(xiàn)的相同功能,且只需要在一個(gè)方法中實(shí)現(xiàn)。由于整個(gè)通知邏輯是在一個(gè)方法內(nèi)實(shí)現(xiàn)的,所以不需要使用成員變量保存狀態(tài)。
修改watchPerformance類:
在切面中,watchPerformance()方法包含了之前四個(gè)通知方法的所有功能。且所有的功能都放在了這一個(gè)方法中,因此這個(gè)方法還要負(fù)責(zé)自身的異常處理。
聲明環(huán)繞通知所需要做的僅僅是使用<aop:around>元素。
使用XML為通知傳遞參數(shù)
使用XML來(lái)配置切面,看一下如何完成這個(gè)任務(wù)。
首先,要移除掉TrackCounter上所有的@AspectJ注解。
去掉@AspectJ注解后,除非顯式調(diào)用countTrack()方法,否則TrackCounter不會(huì)記錄磁道播放的數(shù)量。借助一點(diǎn)Spring XML配置,能夠讓TrackCounter重新變?yōu)榍忻妗?/p>
如下的程序展現(xiàn)了完整的Spring配置,在這個(gè)配置中聲明了TrackCounter bean和BlankDisc bean,并將TrackCounter轉(zhuǎn)化為切面:
切點(diǎn)表達(dá)式中包含了一個(gè)參數(shù),這個(gè)參數(shù)會(huì)傳遞到通知方法中。(不使用“&&”是因?yàn)樵赬ML中,“&”符號(hào)會(huì)被解析為實(shí)體的開始)。
通過(guò)切面引入新的功能
AOP引入并不是AspectJ特有的。使用Spring aop命名空間中的<aop:declare-parents>元素,可以實(shí)現(xiàn)相同的功能。
下面的XML代碼片段與之前基于AspectJ的引入功能相同:
<aop:declare-parents>聲明了此切面所通知的bean要在它的對(duì)象層次結(jié)構(gòu)中擁有新的父類型。類型匹配Performance接口(由types-matching屬性指定)的那些bean在父類結(jié)構(gòu)中會(huì)增加Encoreable接口(由implementinterface屬性指定)。最后要解決的問題是Encoreable接口中的方法實(shí)現(xiàn)要來(lái)自于何處。
有兩種方式標(biāo)識(shí)所引入接口的實(shí)現(xiàn)。本例中,使用default-impl
屬性用全限定類名顯式指定Encoreable的實(shí)現(xiàn)。還可以使用delegate-ref
屬性來(lái)標(biāo)識(shí)。
delegate-ref屬性引用了一個(gè)Spring bean作為引入的委托。需要在Spring上下文中存在一個(gè)ID為encoreableDelegate的bean。
<bean id="encoreableDelegate"
class="concert.DefaultEncoreable" />
使用default-impl來(lái)直接標(biāo)識(shí)委托和間接使用delegate-ref的區(qū)別在于后者是Spring bean,它本身可以被注入、通知或使用其他的Spring配置。
注入AspectJ切面
暫時(shí)跳過(guò)
小結(jié)
AOP是面向?qū)ο缶幊痰囊粋€(gè)強(qiáng)大補(bǔ)充。通過(guò)AspectJ,現(xiàn)在可以把之前分散在應(yīng)用各處的行為放入可重用的模塊中。這有效減少了代碼冗余,并讓我們的類關(guān)注自
身的主要功能。
Spring提供了一個(gè)AOP框架,把切面插入到方法執(zhí)行的周圍。
關(guān)于在Spring應(yīng)用中如何使用切面,可以有多種選擇。
當(dāng)Spring AOP不能滿足需求時(shí),我們必須轉(zhuǎn)向更為強(qiáng)大的AspectJ。