Spring實戰 | 第一部分 Spring的核心(第四章 面向切面的Spring)
第四章 面向切面程式設計
面向切面程式設計的基本原理
通過POJO建立切面
使用@AspectJ註解
為AspectJ切面注入依賴
AspectJ是一個面向切面的框架,它擴充套件了java語言。AspectJ定義了AOP語法,它有一個專門的編譯器用來生成遵循java位元組編碼規範的Class檔案。
在第2章,我們介紹瞭如何使用依賴注入(DI)管理和配置我們的應用物件。DI有助於應用物件之間的解耦,而AOP可以實現橫切關注點與它們所影響的物件之間的解耦。
日誌是應用切面的常見範例,但它並不是切面適用的唯一場景。通過本書,我們還會看到切面鎖適用的多個場景,包括宣告式事務、安全和快取。
本章展示了Spring對切面的支援,包括如何把普通類宣告為一個切面和如何使用註解建立切面。除此之外,我們還會看到AspectJ,另一種流行的AOP實現,如何補充spring AOP框架的功能。但是,我們先不管事務、安全和快取,先看一下spring是如何實現切面的,就從AOP的基礎知識開始說起。
一、什麼是面向切面程式設計
如前所述,切面能幫助我們模組化橫切關注點。簡而言之,橫切關注點可以被,描述為影響應用多處的功能。例如,安全就是一個橫切關注點,應用中的許多方法都會涉及到安全規則,如下圖直觀呈現了橫切關注點的概念。
圖中展示了一個被劃分為模組的典型應用。每個模組的核心功能都是為特定業務領域提供服務,如果在整個應用中都使用相同的基類,繼承往往導致一個脆弱的物件提醒;而使用委託可能需要對委託物件進行復雜的呼叫。
切面提供了取代繼承和委託的另一種可選方案,而且在很多場景下更清晰簡潔。在使用面向切面程式設計時,我們仍然在一個地方定義通用功能。橫切關注點可以被模組化為特殊的類,這些類被稱為切面(aspect)。這樣做有兩個好處:首先,現在每個關注點都集中在一個地方,而不是分散到多處程式碼中;其次,服務模組功能簡潔,因為它們只包含主要關注點(或核心功能)的程式碼,而次要關注點的程式碼被移到切面中。
1、定義AOP術語
AOP已經形成了自己的術語,描述切面的常用術語有通知(advice)、切點(pointcut)和連線點(join point)。下圖展示了這些概念是如何關聯在一起的。
通知(advice)
在AOP術語中,切面的工作被稱為通知,①描述切面要完成工作,②何時執行這個工資。
Spring切面可以應用5種類型的通知:
- 前置通知(Before):在目標方法被呼叫之前呼叫通知功能;
- 後置通知(After):在目標方法完成之後呼叫通知,此時不會關心方法的輸出是什麼;
- 返回通知(After-returning):在目標方法成功執行之後呼叫通知;
- 異常通知(After-throwing):在目標方法丟擲異常後呼叫通知;
- 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法呼叫之前和呼叫之後執行自定義的行為。
連線點(join point)
應用中可能有數以千計的時機應用通知,這些時機被稱為連線點。連線點是在應用執行過程中能夠插入切面的一個點。這個點可以是在呼叫方法時、丟擲異常時、甚至修改一個欄位時。切面程式碼可以利用這些點插入到應用的正常流程之中,並新增新的行為。
切點(pointcut)
切點有助於縮小切面所通知的連線點的範圍。
切點的定義會匹配通知所要織入的一個或多個連線點,我們通常使用明確的類和方法名稱,或是利用正則表示式定義所匹配的類和方法名稱來指定這些切點。有些AOP框架允許我們建立動態的切點,可以根據執行時的決策來決定是否應用通知。
切面(Aspect)
切面是通知和切點的集合。通知和切點共同定義了切面的全部內容,它是什麼,在何時何處完成其功能。
引入(Introduction)
引入允許我們向現有的類新增新方法或屬性。
織入(Weaving)
織入是吧切面應用到目標物件並建立新的代理物件的過程。切面在指定的連線點被織入到目標物件中,在目標物件的生命週期裡有多個點可以進行織入:
- 編譯器:切面在目標類編譯時被織入。這種方式需要特殊的編譯器,AspectJ的織入編譯器就是這種方式織入切面的。
- 類載入期:切面在目標類載入到JVM時被織入。這種方式需要特殊的類載入器(ClassLoader),它可以在目標類被引入應用之前增強該目標類的位元組碼。AspectJ5的載入時織入(load-time weaving,LTW)就支援以這種方式織入切面。
- 執行期:切面在應用執行的某個時刻被織入。一般情況下,在織入切面時,AOP容器會為目標物件動態的載入建立一個代理物件。spring AOP就是以這種方式織入切面的。
2、spring對AOP的支援
Spring提供了4種類型的AOP支援:
- 基於代理的經典spring AOP;
- 純POJO切面;
- @AspectJ註解驅動的切面;
- 注入式AspectJ切面(適用於spring各版本)。
spring通知是java寫的,定義通知所應用的切點通常會使用註解或在spring配置檔案裡採用XML來編寫。
AspectJ與之相反。雖然AspectJ現在支援基於註解的切面,但AspectJ最初是以JAVA語言擴充套件的方式實現的。通過特有的AOP語言,我們可以獲得更強大的細粒度的控制以及更豐富的AOP工具類,但是我們需要額外學習新的工具和語法。
spring在執行時通知物件
通過在代理類中包裹切面,spring在執行期把切面織入到spring管理的bean中。如下圖所示,代理類封裝了目標類,並攔截通知方法的呼叫,再把呼叫轉發給真正的目標bean。當大力攔截到方法呼叫時,在呼叫目標bean方法之前,會執行切面邏輯。
直到應用需要被代理的bean時,spring才建立物件。如果使用的是ApplicationContext的話,在ApplicationContext從beanFactory中載入所有bean的時候,spring才會建立被代理的物件。因為spring執行時才建立代理物件,所以我們需要特殊的編譯器來織入spring AOP的切面。
spring只支援方法級別的連線點
二、通過切點來選擇連線點
在spring AOP中,要使用AspectJ的切點表示式語言來定義切點。如果你已經很熟悉AspectJ,那麼在spring中定義切點就感覺非常自然。但是如果你一點都不瞭解AspectJ的話,本小節我們將快速介紹一下如何編寫AspectJ風格的切點。
關於spring AOP的AspectJ切點,最重要的一點就是spring僅支援AspectJ切點指示器(pointcut designator)的一個子集。
下表列出了spring AOP所支援的AspectJ切點指示器。
AspectJ指示器 | 描述 |
arg() | 限制連線點匹配引數為指定型別的執行方法 |
@args() | 限制連線點匹配引數由指定註解標註的執行方法 |
execution | 用於匹配連線點的執行方法 |
this() | 限制連線點匹配AOP代理的bean引用為指定型別的類 |
target | 限制連線點匹配目標物件為指定型別的類 |
@target() | 限制連線點匹配特定的執行物件,這些物件對應的類要具有特定型別的註解 |
within() | 限制連線點匹配指定的型別 |
@within() | 限制連線點匹配指定註解所標註的型別(當使用spring AOP時,方法定義在由指定的註解所標註的類裡) |
@annotation | 限定匹配帶有指定註解的連線點 |
在spring中嘗試使用AspectJ其他指示器時,將會丟擲IllegalArgument-Exception異常。
當我們檢視如上所展示的這些spring支援的指示器時,注意只有execution指示器時實際執行匹配的,而其它的指示器都是用來限制匹配的。這說明execution指示器時我們在編寫切面定義時最主要使用的指示器。在此基礎上,我們使用其它指示器來限制所匹配的切點。
1、編寫切點
為了闡述spring中的切面,我們需要有個主題來定義切面的切點。為此,我們定義一個Performance介面:
package oschina;
public interface Performance{
public void perform();
}
Performance可以代表任何型別的現場表演,如舞臺劇、電影或音樂會。假設我們想編寫Performance的perform()方法觸發的通知。如下圖展示了一個切點表示式,這個表示式能夠設定當perform()方法執行時觸發通知的呼叫。
我們使用execution()指示器選擇Performance的perform()方法。方法表示式以“*”號開始,表明了我們不關心方法返回值的型別。然後,我們指定了全限定類名和方法名。對於方法引數列表,我們使用兩個點號(..)表明切點要選擇任意的perform()方法,無論該方法的入參是什麼。
現在假設我們需要配置的切點僅匹配concert包。在此場景下,可以使用within()指示器來限制匹配,如下圖所示:
請注意我們使用了“&&”操作符把execution()和within()指示器連線在一起形成與(and)關係(切點必須匹配所有的指示器)。類似地,我們可以使用“||”操作符來標識或(or)關係,而使用“!”操作符來標識非(not)操作。
因為“&”在XML中有特殊含義,所以在Spring的XML配置裡面描述切點時,我們使用and來代替“&&”。同樣,not和or分別帶圖“!”和“||”。
2、在切點中選擇bean
除了上述介紹的指示器外,spring還引入了一個新的bean()指示器,它允許我們在切點表示式中使用bean的ID來標識bean。bean()使用bean ID或bean名稱作為引數來限制切點只匹配特定的bean。
例如,考慮如下的切點:
execution(* concert.Performance.perform()) and bean('woodstock')
在此場景下,切面的通知會被編織到所有ID不為Woodstock的bean中。
三、使用註解建立切面
使用註解來建立切面是AspectJ5引入的關鍵特性。AspectJ5之前,編寫AspectJ切面需要學習一種java語言的擴充套件,但是AspectJ面向註解的模型可以非常簡便的通過少量註解把任意類轉變為切面。
我們已經定義了Performance()介面,它是切面中切點的目標物件。現在,讓我們使用AspectJ註解來定義切面。
1、定義切面
package oschina;
@Aspect
public class Audience{
//表演之前
@Before("execution(** concert.Performance.perform(..))")
public void silenceCellPhones(){
System.out.println("silencing cell phones");
}
//表演之前
@Before("execution(** concert.Performance.perform(..))")
public void takeSeats(){
System.out.println("taking seats");
}
//表演之後
@AfterReturning("execution(** concert.Performance.perform(..))")
public void applause(){
System.out.println("CLAP CLAP CLAP!!!");
}
//表演失敗之後
@AfterThrowing("execution(** concert.Performance.perform(..))")
public void demandRefund(){
System.out.println("Demanding a refund");
}
}
Audience類使用@AspectJ註解進行了標註。該註解宣告Audience不僅僅是一個POJO,還是一個切面。Audience類中的方法都使用註解來定義切面的具體行為。
Audience有四個方法,定義了一個觀眾在觀看演出時可能會做的事情。在演出之前,觀眾要就坐(takeSeats())並將手機調至靜音狀態silenceCellPhones()。如果演出很精彩的話,觀眾應該會鼓掌喝彩applause()。不過演出沒有達到預期的話,觀眾會要求退票demandRefund()。
可以看到,這些方法都使用了通知註解來表明他們應該在什麼時候呼叫。AspectJ提供了五個註解來定義通知,如下圖所示:
spring使用AspectJ註解來宣告通知方法
註解 | 通知 |
@After | 通知方法會在目標方法返回或丟擲異常後呼叫 |
@AfterReturning | 通知方法會在目標方法返回後呼叫 |
@AfterThrowing | 通知方法會在目標方法丟擲異常後呼叫 |
@Around | 通知方法會將目標方法封裝起來 |
@Before | 通知方法會在目標方法呼叫之前執行 |
Audience使用到了前面五個註解中的三個。
相同的切點表示式我們重複寫了四遍,這可不是什麼光彩的事情。
通過@Pointcut註解宣告頻繁使用的切點表示式
package oschina;
@Aspect
public class Audience{
//定義命名的切點
@Pointcut("execution(** concert.Performance.perform(..))")
public void performance(){
}
//表演之前
@Before("performance()")
public void silenceCellPhones(){
System.out.println("silencing cell phones");
}
//表演之前
@Before("performance()")
public void takeSeats(){
System.out.println("taking seats");
}
//表演之後
@AfterReturning("performance()")
public void applause(){
System.out.println("CLAP CLAP CLAP!!!");
}
//表演失敗之後
@AfterThrowing("performance()")
public void demandRefund(){
System.out.println("Demanding a refund");
}
}
在Audience中,performance()方法使用了@Pointcut註解。為@Pointcut註解設定的值是一個切點表示式,就像之前在通知註解上所設定的那樣。通過在performance()方法上新增@Pointcut註解,我們實際上擴充套件了切點表示式語言,這樣就可以在任何的切點表示式中使用performance()了。
需要注意的是,除了註解和沒有實際操作的performance()方法,Audience類依然是一個POJO。我們能夠像其他的Java類那樣呼叫它的方法,它的方法能夠進行獨立的單元測試。
2、建立環繞通知
環繞通知是最為強大的通知型別。它能夠讓你所編寫的邏輯將通知的目標方法完全包裝起來。實際上就像在一個通知方法中同時編寫前置通知和後置通知。
為了闡述環繞通知,我們重寫Autience切面。
@Aspect
public class Audience{
//定義命名的切點
@Pointcut("execution(** concert.Performance.perform(..))")
public void performance(){}
//環繞通知方法
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp){
try{
System.out.println("Silencing cell phones");
System.out.println("Taking seats");
jp.proceed();
System.out.println("CLAP CLAP CLAP!!!");
}catch(Exception e){
System.out.println("Demanding a refund");
}
}
}
在這裡,@Around註解宣告watchPerformance()方法會作為Performance()切點的環繞通知。這個通知所達到的效果和前置通知和後置通知是一樣的。
3、處理通知中的引數
4、通過註解引入新功能
當引入介面的方法被呼叫時,代理會把此呼叫委託給實現了新介面的某個其它物件,實際上,一個bean的實現被拆分到多個類中。
藉助於AOP的引入功能,為了實現該功能,我們要建立一個新的切面:
package oschina;
@Aspect
public class EncoreableIntroducer{
@DeclareParents(value="concert.Performance+",defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}
可以看到EncoreableIntroducer是一個切面,通過@DeclareParents註解,將Encoreable介面引入到Performance bean中。
@DeclareParents註解由三部分組成:
- value屬性指定了哪種型別的bean要引入該介面。在本例中,就是所有Performance型別。(標記符後面的+表示Performance的所有子型別,而不是Performance本身)
- defaultImpl屬性指定了為引入功能提供實現的類。
- @DeclareParents註解所標註的靜態屬性指明瞭要引入的介面。
和其它切面一樣,我們需要在spring應用中將EncoreableIntroducer宣告為一個bean:
<bean class="concert.EncoreableIntroducer" />
spring的自動代理機制將會獲得它的宣告,當spring發現一個bean使用了@Aspect註解時,spring就會建立一個代理,然後將呼叫委託給被代理的bean或被引入的實現,這取決於呼叫的方法屬於被代理的bean還是屬於被引入的介面。
四、在XML中宣告切面
基於註解的配置優於基於java的配置,基於java的配置優於基於XML的配置。
在spring的AOP名稱空間中,提供了多個元素用來在XML中宣告切面,如圖所示:
spring的AOP配置元素能夠以非侵入性的方式宣告切面
AOP配置元素 | 用途 |
<aop:advisor> | 定義AOP通知器 |
<aop:after> | 定義AOP後置通知 |
<aop:after-returning> | 定義AOP返回通知 |
<aop:after-throwing> | 定義AOP異常通知 |
<aop:around> | 定義AOP環繞通知 |
<aop:aspect> | 定義一個切面 |
<aop:aspectj-autoproxy> | 啟用@AspectJ註解驅動的切面 |
<aop:before> | 定義一個AOP前置通知 |
<aop:config> | 頂層的AOP配置元素,大多數的<aop:*>元素必須包含<aop:config>元素內 |
<aop:declare-parents> | 以透明的方式為被通知的物件引入額外的介面 |
<aop:pointcut> | 定義一個切點 |
我們已經看到了<aop:aspectj-autoproxy>元素,它能夠自動代理AspectJ註解的通知類。aop名稱空間的其它元素能夠讓我們直接在spring配置中宣告切面,而不需要使用註解。
1、宣告前置和後置通知
<aop:config>
<aop:aspect ref="audience"> --引用audience bean
<aop:before pointcut="execution(** concert.Performance.perform(..))" method="silencePhones" />
<aop:before pointcut="execution(** concert.Performance.perform(..))" method="takeSeats" />
<aop:after-returning pointcut="execution(** concert.Performance.perform(..))" method="applause" />
<aop:after-throwing pointcut="execution(** concert.Performance.perform(..))" method="demandRefund" />
</aop:aspect>
</aop:config>
關於Spring AOP配置元素,第一個需要注意的事項是大多數的AOP配置元素必須在<aop:config>元素的上下文內使用。
在所有的通知元素中,pointcut屬性定義了通知所應用的切點,它的值是使用AspectJ切點表示式語法鎖定義的切點。
2、宣告環繞通知
使用環繞通知可以完成前置通知和後置通知所實現的相同功能,而且只需要在一個方法中。
watchPerformance()方法提供了AOP環繞通知:
package oschina;
public class Audience{
public void watchPerformance(ProceedingJoinPoint jp){
try{
System.out.println("Silencing cell phone");
System.out.println("Taking seats");
jp.proceed();
}catch(Exception e){
System.out.println("Demanding a refund");
}
}
}
watchPerformance()方法中包含了四個通知方法的所有功能。
在XML中使用<aop:around>元素宣告環繞通知:
<aop:config>
<aop:aspect ref="audience"> --引用audience bean
<aop:before pointcut id="perfoemance" expression="execution(** concert.Performance.perform(..))" />
<aop:around pointcut-ref="performance" method="watchPerformance" />
</aop:aspect>
</aop:config>
像其他通知的XML元素一樣,<aop:around>指定了一個切點和一個通知方法的名字。在這裡,我們使用跟之前一樣的切點,但是為該切點所設定的method屬性值為watchPerformance()。
3、為通知傳遞引數
4、通過切面引入新的功能
5、注入AspectJ切面
public aspect CriticAspect{
public CriticAspect(){}
pointcut perfoemance():execution(* perfoem(...));
afterReturning():performance(){
System.out.println(criticismEngine.getCriticism);
}
private CriticismEngine(CriticismEngine criticismEngine){
this.criticismEngine = criticismEngine;
}
}
CriticAspect的主要職責是在表演結束後為表演發表評論。performance()切點匹配perform()方法。當它與afterReturning()通知一起配合使用時,我們可以讓該切面在表演結束時起作用。
6、小結
AOP是面向物件程式設計的一個強大擴充,通過AspectJ,我們現在可以把之前分散在應用各處的行為放入可重用的模板中,我們顯示的宣告在何處如何應用該行為,這有效的減少了程式碼冗餘,並讓我們額類關注自身的主要功能。
spring提供了一個AOP框架,讓我們把切面插入到方法執行的周圍,我們現在已經學會如何把通知織入前置、後置和環繞方法的呼叫中,以及處理異常增加自定義的行為。
關於spring應用中如何使用切面,我們可以有多種選擇。通過使用@AspectJ註解和簡化的配置名稱空間,在spring中裝配通知和切點變得非常簡單。
我們瞭解瞭如何使用spring為AspectJ切面注入依賴。