1. 程式人生 > >Spring實戰 | 第一部分 Spring的核心(第四章 面向切面的Spring)

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切面注入依賴。

 

Sp