1. 程式人生 > >Spring——面向切面程式設計

Spring——面向切面程式設計

本文主要依據《Spring實戰》第四章內容進行總結

1、面向切面程式設計術語

1.1、橫切關注點

散佈於應用中多處的功能被稱為橫切關注點,通常來講,這些橫切關注點從概念上是與應用的業務邏輯相分離的(但是往往會直接嵌入到應用的業務邏輯中),把這些橫切關注點與業務邏輯相分離正是面向切面程式設計(AOP)所要解決的問題。AOP可以實現橫切關注點與它們所影響的物件之間的解耦。

橫切關注點可以被模組化為特殊的類,這些類被稱為切面(aspect),這樣做有兩個好處:首先,現在每個關注點都集中於一個地方,而不是分散到多處程式碼中;其次,服務模組更簡潔,因為它們只包含主要關注點(或核心功能)的程式碼,而次要關注點的程式碼被轉移到切面中了。

1.2、通知(Advice)

切面的工作被稱為通知,通知定義了切面是什麼以及何時使用。Spring切面可以應用5種類型的通知:

  • 前置通知(Before):在目標方法被呼叫之前呼叫通知功能;
  • 後置通知(After):在目標方法完成之後呼叫通知,此時不會關心方法的輸出是什麼;
  • 返回通知(After-returning):在目標方法成功執行之後呼叫通知;
  • 異常通知(After-throwing):在目標方法丟擲異常後呼叫通知;
  • 環繞通知(Around):通知包裹了被通知的方法,在被通知方法呼叫之前和呼叫之後執行自定義的行為。

1.3、連線點(Join point)

連線點是在應用執行過程中能夠插入切面的一個點,這個點可以是呼叫方法時、丟擲異常時、甚至修改一個欄位時。切面程式碼可以利用這些點插入到應用的正常流程之中,並新增新的行為。

1.4、切點(Pointcut)

一個切面並不需要通知應用的所有連線點,切點有助於縮小切面所通知的連線點的範圍。如果說通知定義了切面的“什麼”和“何時”的話,那麼切點就定義了“何處”。切點的定義會匹配通知所要織入的一個或多個連線點,我們通常使用明確的類和方法名稱,或是利用正則表示式定義所匹配的類和方法名稱來指定這些切點。

1.5、切面(Aspect)

切面是通知和切點的結合,通知和切點共同定義了切面的全部內容——它是什麼,在何時和何處完成其功能。

1.6、引入(Introduction)

引入允許我們向現有的類新增新方法或屬性。

1.7、織入(Weaving)

織入是把切面應用到目標物件並建立新的代理物件的過程,切面在指定的連線點被織入到目標物件中。

1.8、Spring對AOP的支援

Spring提供了4種類型的AOP支援:

  • 基於代理的經典Spring AOP;
  • 純POJO切面;
  • @AspectJ註解驅動的切面;
  • 注入式AspectJ切面

Spring AOP構建在動態代理基礎之上,因此,Spring對AOP的支援侷限於方法攔截,也就是說,Spring只支援方法級別的連線點。

通過在代理類中包裹切面,Spring在執行期把切面織入到Spring管理的bean中,代理類封裝了目標類,並攔截被通知方法的呼叫,再把呼叫轉發給真正的目標bean,當代理攔截到方法的呼叫時,在呼叫目標bean方法之前,會執行切面邏輯。

2、定義切點

切點用於準確定位應該在什麼地方應用切面的通知,在Spring AOP中,要使用AspectJ的切點表示式語言來定義切點。

AspectJ指示器 描述
arg() 限制連線點匹配引數為指定型別的執行方法
@args() 限制連線點匹配引數由指定註解標註的執行方法
excution() 用於匹配是連線點的執行方法
this() 限制連線點匹配AOP代理的bean引用為指定型別的類
target 限制連線點匹配目標物件為指定型別的類
@target 限制連線點匹配特定的執行物件,這些物件對應的類要具有指定型別的註解
within() 限制連線點匹配指定的型別
@within() 限制連線點匹配指定註解所標註的型別(當使用Spring AOP時,方法定義在由指定的註解所標註的類裡)
@annotation 限定匹配帶有指定註解的連線點

只有excution()指示器是實際執行匹配的,而其他指示器都是用來限制匹配的。

2.1、編寫切點

我們定義一個Performance介面:

package concert;

public interface Performance {
    public void perform();
}

假設我們想編寫Performance的perform()方法觸發的通知,我們定義如下的切點:

execution(* concert.Performance.perform(..))

我們使用execution()指示器表示在執行Performance的perform()方法時觸發,方法表示式以*開始,表示我們不關心方法返回值的型別,可以返回任意型別,然後我們指定了全限定類名和方法名,對於方法引數列表,我們使用兩個點號(..)表明切點要選擇任意的perform()方法,無論該方法的入參是什麼。

假如我們需要配置的切點僅匹配concert包,在此場景下,可以使用within()指示器來限制匹配,例如:

execution(* concert.Performance.perform(..)) && within(concert.*)

在這裡我們使用了“&&”操作符把execution()和within()指示器連線在一起形成了與關係,我們也可以使用“||”操作符來標識或關係,使用“!”操作符來標識非操作。

因為“&”在XML中有特殊含義,所以在Spring的XML配置裡描述切點時,我們可以使用and來代替“&&”,同樣,or和not可以分別用來代替“||”和“!”。

Spring還引入了一個新的bean()指示器,它允許我們在切點表示式中使用bean的ID來標識bean。bean()使用bean ID或bean名稱作為引數來限制切點只匹配特定的bean。例如:

execution(* concert.Performance.perform()) and bean(‘woodstock’)

在這裡,我們希望在執行Performance的perform()方法時應用通知,但限定bean的ID為woodstock。

3、使用註解建立切面

3.1、使用註解建立切面

我們定義如下一個Audience類,它定義了一個切面:

@Aspect
public class Audience {

    @Before("execution(** concert.Performance.perform(..))")
    public void silenceCellPhones() {
        System.out.println("請關閉手機");
    }

    @Before("execution(** concert.Performance.perform(..))")
    public void takeSeats() {
        System.out.println("請入座");
    }

    @AfterReturning("execution(** concert.Performance.perform(..))")
    public void applause() {
        System.out.println("熱烈鼓掌");
    }

    @AfterThrowing("execution(** concert.Performance.perform(..))")
    public void demandRefund() {
        System.out.println("要求退款");
    }

}

可以看到,Audience類使用了@Aspect註解進行標註,@Aspect註解用來表明這個類不僅是一個POJO,還是一個切面。這個類有四個方法,定義了一個觀眾在觀看錶演時可能會做的事,在演出之前需要入座(taksSeats)、關閉手機(silenceCellPhones),演出很精彩的話,觀眾會鼓掌喝彩(applause),如果演出沒有達到預期效果,觀眾會要求退款(demandRefund)。這些方法都使用了通知註解表明它們應該在什麼時候呼叫,AspectJ提供了五個註解來定義通知:

註解 通知
@After 通知方法會在目標方法返回或丟擲異常後呼叫
@AfterReturning 通知方法會在目標方法返回後呼叫
@AfterThrowing 通知方法會在目標方法丟擲異常後呼叫
@Around 通知方法會將目標方法封裝起來
@Before 通知方法會在目標方法呼叫之前執行

在上面這個類裡,每個通知註解都給定了一個切點表示式作為它的值,但是可以注意到,它們的切點表示式是相同的,也就是說相同的切點表示式在一個切面中定義了多次,這不是一個很好的方案。我們可以使用@Pointcut註解來定義一個可重用的切點:

@Aspect
public class Audience {

    @Pointcut("execution(** concert.Performance.perform(..))")
    public void performance() {

    }

    @Before("performance()")
    public void silenceCellPhones() {
        System.out.println("請關閉手機");
    }

    @Before("performance()")
    public void takeSeats() {
        System.out.println("請入座");
    }

    @AfterReturning("performance()")
    public void applause() {
        System.out.println("熱烈鼓掌");
    }

    @AfterThrowing("performance()")
    public void demandRefund() {
        System.out.println("要求退款");
    }

}

可以看到,@Pointcut註解設定的值是一個切點表示式,在performance()方法上新增@Pointcut註解實際上是擴充套件了切點表示式語言,performance()方法的實際內容並不重要,在這裡它實際上是空的,該方法只是一個標識,供@Pointcut註解依附。

在前面的例子中,我們使用@Aspect將Audience類宣告為一個切面,但是如果要在Spring中使用這個切面,還需要將其裝配為Spring中的bean:

@Aspect
@Component
public class Audience {
    ……
}

在這裡,我們使用@Component註解將Audience宣告為Spring中的一個bean,但是要注意的是,這樣配置也只會在Spring容器中宣告一個bean,即使它使用了@Aspect註解,它也不會被視為切面。這些註解不會生效,需要配置使切面生效。

如果使用JavaConfig配置Spring,可以在配置類的類級別上通過使用@EnableAspectJAutoProxy註解啟動自動代理功能:

@Configuration
@ComponentScan(basePackageClasses={Audience.class, Performance.class})
@EnableAspectJAutoProxy
public class AspectConfig {
}

如果是使用XML來裝配bean的話,那麼就需要使用Spring aop名稱空間中的<aop:aspectj-autoproxy> :

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-2.5.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:component-scan base-package="concert,aspect" />

    <!-- 啟用AspectJ自動代理 -->
    <aop:aspectj-autoproxy />


</beans>

3.1、建立環繞通知

環繞通知是最為強大的通知型別,它能夠讓你所編寫的邏輯將被通知的目標方法完全包裝起來,實際上就像在一個通知方法中同時編寫前置通知和後置通知。例如我們可以使用環繞通知代替之前多個不同的前置通知和後置通知:

@Aspect
public class AroundAudience {

    @Pointcut("execution(** concert.Performance.perform(..))")
    public void performance() {

    }

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint jp) {  
        try {
            System.out.println("請關閉手機");
            System.out.println("請入座");
            jp.proceed();
            System.out.println("熱烈鼓掌");
        } catch (Throwable e) {
            System.out.println("要求退款");
        }   
    }

}

在這裡,@Around註解表明watchPerformance()方法會作為performance()切點的環繞通知,在這個通知中,觀眾在演出之前關閉手機並就坐,演出結束後會鼓掌喝彩,像前面一樣,如果演出失敗的話,觀眾會要求退款。

watchPerformance()這個通知方法會接受ProceedingJoinPoint作為引數,這個物件是必須要有的,因為要在通知中通過它來呼叫被通知的方法,通知方法中可以做任何事情,當要將控制權交給被通知方法時,它需要呼叫ProceedingJoinPoint的proceed()方法,如果不呼叫這個方法的話,實際上會阻塞被通知方法的呼叫。

3.2、處理通知中的引數

目前我們所寫的通知方法不需要關注傳遞給被通知方法的任意引數,但是,如果切面所通知的方法確實有引數該怎麼辦呢?切面能訪問和使用傳遞給被通知方法的引數嗎?

我們可以通過以下例項來講解如何處理通知中的引數,我們定義一個BlankDisc類,在這個類中通過playTrack()方法來播放指定磁軌中的歌曲:

public class BlankDisc {
    private String title;
    private String artist;
    private List<String> tracks;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getArtist() {
        return artist;
    }

    public void setArtist(String artist) {
        this.artist = artist;
    }

    public List<String> getTracks() {
        return tracks;
    }

    public void setTracks(List<String> tracks) {
        this.tracks = tracks;
    }

    public void playTrack(int trackNum) {
        System.out.println(tracks.get(trackNum - 1));
    }
}

現在我們想記錄每個磁軌的被播放次數,一種方法是修改playTrack()方法,直接在每次呼叫的時候記錄播放次數,但是記錄播放次數和播放是不同的關注點,因此可以使用切面來完成,所以我們建立TrackCounter類,它是通知playTrack()方法的一個切面:

@Aspect
public class TrackCounter {

    private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();

    @Pointcut("execution(* concert.BlankDisc.playTrack(int)) && args(trackNum)")
    public void trackPlayed(int trackNum) {
    }

    @Before("trackPlayed(trackNum)")
    public void countTrack(int trackNum) {
        int currenttCount = getPlayCount(trackNum);
        trackCounts.put(trackNum, currenttCount + 1);
    }

    public int getPlayCount(int trackNum) {
        return trackCounts.containsKey(trackNum) ? trackCounts.get(trackNum) : 0;
    }
}

在這裡,我們使用@Pointcut聲明瞭一個切點,在這個切點宣告中,除了使用execution()表示式來指定匹配的執行方法,我們還使用了args()表示式來表明傳遞給playTrack()方法的int型別引數也會傳遞到通知中去,引數的名稱也與切點方法簽名中的引數相匹配,同時,切點定義中的引數與切點方法中的引數名稱是一樣的,這樣就完成了從命名切點到通知方法引數的轉移。通俗點說,就是args()限定符中的引數名稱、切點定義中的引數名稱和切點方法中的引數名稱三者需要統一,這樣就能使被通知方法的實參傳遞給通知方法。接下來我們就來驗證一下,我們首先將這兩個bean宣告為Spring中的bean:

@Configuration
@EnableAspectJAutoProxy
public class AspectConfig {
    @Bean
    public BlankDisc blankDisc() {
        BlankDisc cd = new BlankDisc();

        cd.setTitle("音樂名稱");
        cd.setArtist("音樂作者");
        List<String> tracks = new ArrayList<String>();
        tracks.add("音樂1");
        tracks.add("音樂2");
        tracks.add("音樂3");
        tracks.add("音樂4");
        tracks.add("音樂5");
        tracks.add("音樂6");
        cd.setTracks(tracks);

        return cd;
    }

    @Bean
    public TrackCounter trackCounter() {
        return new TrackCounter();
    }   
}

在這裡我們預設為BlankDisc添加了幾首歌曲,接下來使用JUnit進行測試:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=AspectConfig.class)
public class AspectTest {
    @Autowired
    private BlankDisc cd;

    @Autowired
    private TrackCounter t;

    @Test
    public void test() {
        cd.playTrack(1);
        cd.playTrack(1);
        cd.playTrack(3);
        cd.playTrack(4);
        cd.playTrack(4);
        cd.playTrack(4);
        cd.playTrack(5);
        cd.playTrack(6);
        cd.playTrack(6);

        for(String s : cd.getTracks()) {
            System.out.println(s + "播放" + t.getPlayCount(cd.getTracks().indexOf(s) + 1) + "次");
        }
    }

}

執行測試方法,執行結果如下:

音樂1
音樂1
音樂3
音樂4
音樂4
音樂4
音樂5
音樂6
音樂6
音樂1播放2次
音樂2播放0次
音樂3播放1次
音樂4播放3次
音樂5播放1次
音樂6播放2次

可以看到,TrackCounter是能夠正確記錄相應磁軌被播放的次數的。

3.3、通過註解引入新功能

在Spring中,切面只是實現了它們所包裝bean相同介面的代理,如果除了實現這些介面,代理也能暴露新介面的話,會怎麼樣呢?那樣的話,切面所通知bean看起來像是實現了新的介面,即便底層實現類並沒有實現這些介面也無所謂。當引入介面的方法被呼叫時,代理會把此類呼叫委託給實現了新介面的某個其他物件,實際上,一個bean的實現被拆分到多個類中。我們看一下下面這個例子,我們定義一個介面Encoreable:

public interface Encoreable {
    public void performEncore();
}

這個介面定義了一個返場演出的方法,接下來我們在切面中新增一段程式碼:

    @DeclareParents(value="concert.Performance",defaultImpl=DefaultEncoreable.class)
    public static Encoreable encoreable;

這段程式碼將@DeclareParents註解將Encoreable介面引入到Performance bean中,@DeclareParents註解由三部分組成:

  • value屬性指定了那種型別的bean要引入該介面,在這裡我們使用的是concert.Performance,我們也可以使用concert.Performance+,在這裡加號表示是Performance的所有子型別,而不是Performance本身。
  • defaultImpl屬性指定了為引入功能提供實現的類。
  • @DeclareParents註解所標註的靜態屬性指明瞭要引入的介面。

這樣宣告之後,Spring的自動代理機制將會獲取到它的宣告,當Spring發現一個bean被呼叫時,Spring會將呼叫委託給被代理的bean或被引入的實現,這取決於呼叫的方法屬於被代理的bean還是屬於被引入的介面。我們這樣進行測試:

    @Autowired
    private Performance p;

    @Test
    public void test(){
        p.perform();
        Encoreable e = (Encoreable) p;
        e.performEncore();
    }

當需要呼叫Performance的perform()方法時,這個呼叫會被傳遞給Performance bean,如果要呼叫performEncore()方法,Spring會將這個呼叫委託給DefaultEncoreable。

這一節主要介紹了使用註解建立切面,使用註解的切面宣告有一個明顯的劣勢:必須能夠為通知類添加註解,為了做到這一點,必須要有原始碼,如果沒有原始碼的話,我們可以使用XML宣告切面。

4、在XML中宣告切面

在Spring的aop名稱空間中,提供了多個元素用來在XML中宣告切面:

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:config> 頂層AOP配置元素,大多數的<aop:*>元素必須包含在<aop:config>元素內
<aop:declare-parents> 以透明的方式為被通知的物件引入額外的介面
<aop:pointcut> 定義一個切點
<aop:before> 定義一個AOP前置通知

我們已經看過了<aop:aspectj-autoproxy> 元素,它能夠自動代理AspectJ註解的通知類,aop名稱空間的其他元素能夠讓我們直接在Spring配置中宣告切面,而不需要使用註解。例如,之前定義的Audience,我們將其中的AspectJ註解全部移除掉:

public class Audience {

    public void silenceCellPhones() {
        System.out.println("請關閉手機");
    }

    public void takeSeats() {
        System.out.println("請入座");
    }

    public void applause() {
        System.out.println("熱烈鼓掌");
    }

    public void demandRefund() {
        System.out.println("要求退款");
    }

}

可以看到,Audience就是一個簡單的Java類,我們可以像其它類一樣將它註冊為Spring應用上下文中的bean,儘管看起來並沒有什麼差別,但Audience已經具備了成為AOP通知的所有條件,我們可以通過aop名稱空間的元素,將其宣告為一個切面。

4.1、宣告前置通知和後置通知

我們可以使用aop名稱空間的元素宣告前置通知和後置通知,下面就是我們通過XML元素將無註解的Audience宣告為一個切面:

<aop:config>
        <aop:aspect ref="audience">
            <aop:before method="silenceCellPhones" pointcut="execution(** concert.Performance.perform(..))"/>

            <aop:before method="takeSeats" pointcut="execution(** concert.Performance.perform(..))"/>

            <aop:after-returning method="applause" pointcut="execution(** concert.Performance.perform(..))"/>

            <aop:after-throwing method="demandRefund" pointcut="execution(** concert.Performance.perform(..))"/>
        </aop:aspect>
</aop:config>

值得注意的是,大多數AOP配置元素必須在<aop:aspect> 元素的上下文內使用,把bean宣告為一個切面時,我們總是從<aop:aspect> 元素開始配置的。在<aop:aspect> 元素內,我們可以宣告一個或多個通知器、切面或者切點。

在上面這個例子中,我們使用<aop:aspect> 元素聲明瞭一個簡單的切面,ref元素引用了一個POJO bean,該bean實現了切面的功能——在這裡就是audience,ref元素所引用的bean提供了在切面中通知所呼叫的方法。我們一共定義了四個不同的通知,兩個<aop:before> 元素定義了匹配切點的方法執行之前呼叫前置通知方法——也就是Audience的silencenCellPhones()和takeSeats()方法(由method屬性指定)。<aop:after-returning> 元素定義了一個返回通知,在切點所匹配的方法呼叫之後再呼叫applause()方法,同樣<aop:after-throwing> 元素定義了異常通知,如果所匹配的方法執行時丟擲任何的異常,都將會呼叫demandRefund()方法。在所有的通知元素中,pointcut屬性定義了通知所應用的切點。

可以看到,如果在通知定義中使用pointcut元素定義切點,如果多個通知的切點是相同的,那麼就會出現重複定義的情況,為了消除重複定義切點,在基於XML的切面宣告中,我們需要使用<aop:pointcut> 元素。我們修改上面的XML配置:

    <aop:config>
        <aop:aspect ref="audience">
            <aop:pointcut expression="execution(** concert.Performance.perform(..))" id="performance"/>
            <aop:before method="silenceCellPhones" pointcut-ref="performance"/>

            <aop:before method="takeSeats" pointcut-ref="performance"/>

            <aop:after-returning method="applause" pointcut-ref="performance"/>

            <aop:after-throwing method="demandRefund" pointcut-ref="performance"/>
        </aop:aspect>
    </aop:config>

我們使用<aop:pointcut> 元素定義了一個id為performance的切點,同時修改了所有的通知元素,用pointcut-ref屬性來引用這個命名切點。在上面這個例子中,<aop:pointcut> 元素定義在<aop:aspect> 元素內,所以<aop:pointcut> 所定義的切點可以被同一個<aop:aspect> 元素之內的所有通知元素引用。如果想讓定義的切點能夠在多個切面中使用,我們可以把<aop:pointcut> 元素放在<aop:config> 元素範圍內。

4.2、宣告環繞通知

我們也可以使用XML宣告環繞通知,環繞通知的一個明顯的優勢就是我們只需一個方法就可以完成前置通知和後置通知所實現的相同功能。我們重新定義Audience類,使用watchPerformance()方法提供AOP環繞通知的功能:

public class Audience {
    public void watchPerformance(ProceedingJoinPoint jp) {

        try {
            System.out.println("請關閉手機");//表演之前
            System.out.println("請入座");//表演之前
            jp.proceed();   //執行被通知的方法
            System.out.println("熱烈鼓掌");//表演成功之後
        } catch (Throwable e) {
            System.out.println("要求退款");//表演失敗之後
        }

    }
}

宣告環繞通知與宣告其他型別的通知並沒有太大的區別,我們需要做的僅僅是使用<aop:around> 元素:

    <aop:config>
        <aop:aspect ref="audience">
            <aop:pointcut expression="execution(** concert.Performance.perform(..))" id="performance"/>

            <aop:around method="watchPerformance" pointcut-ref="performance"/>
        </aop:aspect>
    </aop:config>

4.3、為通知傳遞引數

在上節中,我們使用AspectJ註解建立了一個切面,這個切面能夠記錄每個磁軌的播放次數,現在,我們使用XML來配置切面來完成相同的任務。

首先我們宣告切面TrackCounter,它是無註解的切面:

public class TrackCounter {
    private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();

    public void countTrack(int trackNum) {
        int currenttCount = getPlayCount(trackNum);
        trackCounts.put(trackNum, currenttCount + 1);
    }

    public int getPlayCount(int trackNum) {
        return trackCounts.containsKey(trackNum) ? trackCounts.get(trackNum) : 0;
    }
}

接下來我們使用XML配置切面:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-2.5.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="blankDisc" class="concert.BlankDisc" >
        <property name="title" value="音樂標題" />
        <property name="artist" value="音樂作者" />
        <property name="tracks">
            <list>
                <value>音樂1</value>
                <value>音樂2</value>
                <value>音樂3</value>
                <value>音樂4</value>
                <value>音樂5</value>
                <value>音樂6</value>
            </list>
        </property>
    </bean>

    <bean id="trackCounter" class="aspect.TrackCounter" />

    <aop:config>
        <aop:aspect ref="trackCounter">
            <aop:pointcut expression="execution(* concert.BlankDisc.playTrack(int)) and args(trackNum)" id="trackPlayed"/>

            <aop:before method="countTrack" pointcut-ref="trackPlayed"/>
        </aop:aspect>
    </aop:config>

</beans>

首先我們使用<bean> 元素將BlankDisc和TrackCounter宣告為Spring應用上下文中的bean,然後我們使用aop名稱空間宣告切面,這裡宣告的切面和之前使用XML元素宣告切面是類似的,唯一明顯的差別在於切點表示式中包含了一個引數,這個引數會傳遞到通知方法中。

4.4、通過切面引入新的功能

在上一節,我們使用@DeclareParents註解為被通知的方法引入新的方法,但是AOP引入並不是AspectJ特有的,使用Spring aop名稱空間中的<aop:declare-parents> 元素,我們可以實現相同的功能:

<aop:aspect>
    <aop:declare-parents types-matching="concert.Performance" implement-interface="concert.Encoreable" default-impl="concert.DefaultEncoreable"/>
</aop:aspect>

顧名思義,<aop:declare-parents> 聲明瞭此切面所通知的bean要在它的物件層次結構中擁有新的父型別。在本例中,型別匹配Pefrormance(由types-matching屬性指定)在父類結構中會增加Encoreable介面(由implement-interface屬性指定),最後由default-impl指定預設實現類。

在XML配置中,有兩種方式來標識所引入介面的實現,我們使用default-impl屬性用全限定類名來顯式指定Encoreable的實現,還可以使用delegate-ref引用一個Spring bean作為引入的委託:

    <bean id="defaultEncoreable" class="concert.DefaultEncoreable" />
    <aop:config>
        <aop:aspect>
            <aop:declare-parents types-matching="concert.Performance" implement-interface="concert.Encoreable" delegate-ref="defaultEncoreable"/>
        </aop:aspect>
    </aop:config>

使用default-impl來直接標識委託和間接使用delegate-ref的區別在於後者是Spring bean,它本身可以被注入、通知或者使用其他的Spring配置。

5、注入AspectJ切面

雖然Spring AOP能夠滿足許多應用的切面需求,但是與AspectJ相比,Spring AOP還是一個功能比較弱的AOP解決方案,AspectJ提供了Spring AOP所不能支援的許多型別的切點,例如構造器切點。

許多精心設計且有意義的切面很有可能依賴其它類來完成它們的工作,如果執行通知時,切面依賴於一個或多個類,我們可以在切面內部例項化這些協作物件,但更好的方式是,我們可以藉助Spring的依賴注入把bean裝配進入AspectJ切面中。下面我們通過一個例子看一下,我們使用AspectJ的方式建立一個評論員切面,他會在演出之後提供一些批評意見,下面是這樣一個切面:

public aspect CriticAspect {

    pointcut performance() : execution(* aop.Performance.perform(..));

    after() returning() : performance() {
        System.out.println(criticismEngine.getCriticism());
    }

    private CriticismEngine criticismEngine;

    public void setCriticismEngine(CriticismEngine criticismEngine) {
        this.criticismEngine = criticismEngine;
    }
}

可以看到,這個切面不是普通的Java類,我們將這段程式碼複製到Eclipse中也不能通過編譯,這是一段AspectJ風格的程式碼,為了能夠在Eclipse中開發AspectJ程式碼,我們需要匯入AJDT外掛,安裝方法參考:https://blog.csdn.net/aitangyong/article/details/50770085 這篇文章,首先我們要檢視Eclipse版本,點選Help->About Eclipse:

這裡寫圖片描述

在這裡,我們的Eclipse是4.4的版本,接下來我們需要在AJDT下載網站 中查詢適合相應Eclipse版本的外掛,然後在Eclipse中下載相應的外掛:

這裡寫圖片描述

下載完成後,重啟Eclipse後我們就可以建立AspectJ風格的程式碼了:

這裡寫圖片描述

說回上面的切面定義,我們首先定義了一個切點performance(),這個切點在Performance的perform()方法正確返回之後執行,它會輸出評論員的評論,在這裡,評論員可以通過屬性注入的方式注入進這個切面,我們先來看一下評論員的定義:

public class CriticismEngine {

    private String[] critics = {"表演太精彩了!", "表演很失敗!", "表演一般般"};

    public String getCriticism() {
        int i = (int) (Math.random() * critics.length);
        return critics[i];
    }

}

這只是一個普通的Java類,它能夠隨機輸出一個評論,然後我們再看看在XML中的配置:

    <bean id="criticismEngine" class="aop.CriticismEngine" />

    <bean class="aop.CriticAspect" factory-method="aspectOf">
        <property name="criticismEngine" ref="criticismEngine" />
    </bean>

我們首先將CriticismEngine宣告為一個Spring上下文中的bean,然後使用<bean> 配置CriticAspect,通過屬性注入將CriticismEngine注入進來,乍看下來,這裡的配置和普通的XML配置沒有什麼不同,但是值得注意的是,CriticAspect使用了factory-method屬性,通常情況下,Spring bean由Spring容器初始化,但是AspectJ切面是由AspectJ在執行期間建立的,等到Spring有機會為CriticAspect注入到CriticismEngine時,CriticAspect已經被例項化了。

因為Spring不能負責建立CriticAspect,那就不能在Spring中簡單地把CriticAspect宣告為一個bean。我們需要一種方式為Spring獲得已經由AspectJ建立的CriticAspect例項的控制代碼,從而可以注入CriticismEngine。幸好,所有的AspectJ切面都提供了一個靜態的aspectOf()方法,該方法返回切面的一個單例,所以為了獲取切面的例項,我們必須使用factory-method來呼叫aspectOf()方法,而不是呼叫CriticAspect的構造方法。

Spring不能像之前那樣使用<bean> 宣告來建立一個CriticAspect例項——它已經在執行時由AspectJ建立完成了,Spring需要通過aspectOf()工廠方法獲得切面的引用,然後像<bean> 元素規定的那樣在該物件上執行依賴注入。