Spring實戰 | 第一部分 Spring的核心(第四章 面向切面的Spring)

第四章 面向切面編程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種類型的通知:

  • 前置通知(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就是以這種方式織入切面的。

二、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只支持方法級別的鏈接點

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註解由三部分組成:

  • value屬性指定了哪一種類型的bean要引入該接口。在本例中,就是全部Performance類型。(標記符後面的+表示Performance的全部子類型,而不是Performance自己)
  • defaultImpl屬性指定了爲引入功能提供實現的類。
  • @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切面注入依賴。

 

Spring實戰@目錄

相關文章
相關標籤/搜索