第四章 面向切面編程java
面向切面編程的基本原理正則表達式
經過POJO建立切面spring
使用@AspectJ註解express
爲AspectJ切面注入依賴編程
AspectJ是一個面向切面的框架,它擴展了java語言。AspectJ定義了AOP語法,它有一個專門的編譯器用來生成遵循java字節編碼規範的Class文件。緩存
在第2章,咱們介紹瞭如何使用依賴注入(DI)管理和配置咱們的應用對象。DI有助於應用對象之間的解耦,而AOP能夠實現橫切關注點與它們所影響的對象之間的解耦。安全
日誌是應用切面的常見範例,但它並非切面適用的惟一場景。經過本書,咱們還會看到切面鎖適用的多個場景,包括聲明式事務、安全和緩存。app
本章展現了Spring對切面的支持,包括如何把普通類聲明爲一個切面和如何使用註解建立切面。除此以外,咱們還會看到AspectJ,另外一種流行的AOP實現,如何補充spring AOP框架的功能。可是,咱們先無論事務、安全和緩存,先看一下spring是如何實現切面的,就從AOP的基礎知識開始提及。框架
1、什麼是面向切面編程模塊化
如前所述,切面能幫助咱們模塊化橫切關注點。簡而言之,橫切關注點能夠被,描述爲影響應用多處的功能。例如,安全就是一個橫切關注點,應用中的許多方法都會涉及到安全規則,以下圖直觀呈現了橫切關注點的概念。
圖中展現了一個被劃分爲模塊的典型應用。每一個模塊的核心功能都是爲特定業務領域提供服務,若是在整個應用中都使用相同的基類,繼承每每致使一個脆弱的對象提醒;而使用委託可能須要對委託對象進行復雜的調用。
切面提供了取代繼承和委託的另外一種可選方案,並且在不少場景下更清晰簡潔。在使用面向切面編程時,咱們仍然在一個地方定義通用功能。橫切關注點能夠被模塊化爲特殊的類,這些類被稱爲切面(aspect)。這樣作有兩個好處:首先,如今每一個關注點都集中在一個地方,而不是分散到多處代碼中;其次,服務模塊功能簡潔,由於它們只包含主要關注點(或核心功能)的代碼,而次要關注點的代碼被移到切面中。
一、定義AOP術語
AOP已經造成了本身的術語,描述切面的經常使用術語有通知(advice)、切點(pointcut)和鏈接點(join point)。下圖展現了這些概念是如何關聯在一塊兒的。
通知(advice)
在AOP術語中,切面的工做被稱爲通知,①描述切面要完成工做,②什麼時候執行這個工資。
Spring切面能夠應用5種類型的通知:
鏈接點(join point)
應用中可能有數以千計的時機應用通知,這些時機被稱爲鏈接點。鏈接點是在應用執行過程當中可以插入切面的一個點。這個點能夠是在調用方法時、拋出異常時、甚至修改一個字段時。切面代碼能夠利用這些點插入到應用的正常流程之中,並添加新的行爲。
切點(pointcut)
切點有助於縮小切面所通知的鏈接點的範圍。
切點的定義會匹配通知所要織入的一個或多個鏈接點,咱們一般使用明確的類和方法名稱,或是利用正則表達式定義所匹配的類和方法名稱來指定這些切點。有些AOP框架容許咱們建立動態的切點,能夠根據運行時的決策來決定是否應用通知。
切面(Aspect)
切面是通知和切點的集合。通知和切點共同定義了切面的所有內容,它是什麼,在什麼時候何處完成其功能。
引入(Introduction)
引入容許咱們向現有的類添加新方法或屬性。
織入(Weaving)
織入是吧切面應用到目標對象並建立新的代理對象的過程。切面在指定的鏈接點被織入到目標對象中,在目標對象的生命週期裏有多個點能夠進行織入:
二、spring對AOP的支持
Spring提供了4種類型的AOP支持:
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只支持方法級別的鏈接點
2、經過切點來選擇鏈接點
在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指示器時咱們在編寫切面定義時最主要使用的指示器。在此基礎上,咱們使用其它指示器來限制所匹配的切點。
一、編寫切點
爲了闡述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分別帶圖「!」和「||」。
二、在切點中選擇bean
除了上述介紹的指示器外,spring還引入了一個新的bean()指示器,它容許咱們在切點表達式中使用bean的ID來標識bean。bean()使用bean ID或bean名稱做爲參數來限制切點只匹配特定的bean。
例如,考慮以下的切點:
execution(* concert.Performance.perform()) and bean('woodstock')
在此場景下,切面的通知會被編織到全部ID不爲Woodstock的bean中。
3、使用註解建立切面
使用註解來建立切面是AspectJ5引入的關鍵特性。AspectJ5以前,編寫AspectJ切面須要學習一種java語言的擴展,可是AspectJ面向註解的模型能夠很是簡便的經過少許註解把任意類轉變爲切面。
咱們已經定義了Performance()接口,它是切面中切點的目標對象。如今,讓咱們使用AspectJ註解來定義切面。
一、定義切面
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類那樣調用它的方法,它的方法可以進行獨立的單元測試。
二、建立環繞通知
環繞通知是最爲強大的通知類型。它可以讓你所編寫的邏輯將通知的目標方法徹底包裝起來。實際上就像在一個通知方法中同時編寫前置通知和後置通知。
爲了闡述環繞通知,咱們重寫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()切點的環繞通知。這個通知所達到的效果和前置通知和後置通知是同樣的。
三、處理通知中的參數
四、經過註解引入新功能
當引入接口的方法被調用時,代理會把此調用委託給實現了新接口的某個其它對象,實際上,一個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註解由三部分組成:
和其它切面同樣,咱們須要在spring應用中將EncoreableIntroducer聲明爲一個bean:
<bean class="concert.EncoreableIntroducer" />
spring的自動代理機制將會得到它的聲明,當spring發現一個bean使用了@Aspect註解時,spring就會建立一個代理,而後將調用委託給被代理的bean或被引入的實現,這取決於調用的方法屬於被代理的bean仍是屬於被引入的接口。
4、在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配置中聲明切面,而不須要使用註解。
一、聲明前置和後置通知
<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切點表達式語法鎖定義的切點。
二、聲明環繞通知
使用環繞通知能夠完成前置通知和後置通知所實現的相同功能,並且只須要在一個方法中。
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()。
三、爲通知傳遞參數
四、經過切面引入新的功能
五、注入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()通知一塊兒配合使用時,咱們可讓該切面在表演結束時起做用。
六、小結
AOP是面向對象編程的一個強大擴充,經過AspectJ,咱們如今能夠把以前分散在應用各處的行爲放入可重用的模板中,咱們顯示的聲明在何處如何應用該行爲,這有效的減小了代碼冗餘,並讓咱們額類關注自身的主要功能。
spring提供了一個AOP框架,讓咱們把切面插入到方法執行的周圍,咱們如今已經學會如何把通知織入前置、後置和環繞方法的調用中,以及處理異常增長自定義的行爲。
關於spring應用中如何使用切面,咱們能夠有多種選擇。經過使用@AspectJ註解和簡化的配置命名空間,在spring中裝配通知和切點變得很是簡單。
咱們瞭解瞭如何使用spring爲AspectJ切面注入依賴。