Spring系列(三)Spring AOP詳解

1 什麼是面向切面編程

在軟件開發中,散佈於應用中多處的功能被稱爲 橫切關注點(cross-cutting concern)面向切面編程(AOP)解決的問題就是把橫切關注點與業務邏輯相分離。

在使用面向切面編程時,我們仍然在一個地方定義通用功能,但是可以通過聲明的方式定義這個功能要以何種方式在何處應用,而且無需修改受影響的類。

橫切關注點可以模塊化特殊的類,這些類被稱爲切面(aspect)。這樣做有兩個好處:

  • 每個關注點都集中於一個地方,而不是分散到多處代碼中;
  • 服務模塊更簡潔,因爲它們只包含主要關注點(或核心功能)的代碼,而次要關注點的代碼被轉移到切面中。

簡單來講,Spring的AOP目的是爲了解耦。AOP可以讓一組類共享相同的行爲。在OOP中只能通過繼承類和實現接口,來使代碼的耦合度增加,且類繼承只能爲單繼承。

1.1 定義AOP術語

描述切面的常用術語有:

  • 通知:advice
  • 切點:pointcut
  • 連接點:join point

在一個或者多個連接點上,可以把切面的功能(通知)織入到程序的執行過程中。

通知定義了切面是什麼以及何時使用。

spring切面可以應用的5種類型的通知:

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

連接點是在應用執行過程中能夠插入切面的一個點。這個點可以是調用方法時、拋出異常時、甚至修改一個字段時。切面代碼可以利用這些點插入到應用的正常流程之中,並添加新的行爲。

切點(pointcut)定義了在何處插入切面,切點的定義會匹配通知所要織入的一個或者多個連接點。我們通常使用明確的類和方法名稱或者正則表達式定義所匹配的類和方法名稱來指定這些切點。

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

引入(Introduction)
引入允許我們向現有的類添加新的方法或屬性。

織入(Weaving)
織入是把切面應用到目標對象並創建新的代理對象的過程。切面在指定的連接點被織入到目標對象中。在目標對象的生命週期裏有多個點可以進行織入:

編譯期:切面在目標類編譯時被織入。這種方式需要特殊的編譯器。AspectJ的織入編譯器 就是以這種方式織入切面的。

類加載期:切面在目標類加載到JVM時被織入。這種方式需要特殊的類加載器 (ClassLoader),它可以在目標類被引入應用之前增強該目標類的字節碼。AspectJ 5的 加載時織入(load-time weaving,LTW)就支持以這種方式織入切面。

運行期:切面在應用運行的某個時刻被織入。一般情況下,在織入切面時,AOP容器會爲目標對象動態地創建一個代理對象。**Spring AOP就是以這種方式織入切面的。 **

1.2 Spring對AOP的支持

Spring提供了四種類型的AOP支持:

  • 基於代理的經典Spring AOP;
  • 純POJO切面;
  • @AspectJ註解驅動的切面;
  • 注入式AspectJ切面(使用於Spring各版本)

前三種都是Spring AOP實現的變體,Spring AOP構建在動態代理的基礎上,因此,Spring對AOP的支持侷限於方法的攔截。

藉助Spring AOP 的命名空間,我們可以將純POJO轉換爲切面。這些POJO只是提供了滿足切點條件時所要調用的方法。這種技術需要使用XML配置。

Spring借鑑了AspectJ的切面,以提供註解驅動的AOP。本質上,它依然是Spring基於代理的AOP,但是編程模型幾乎與編寫成熟的AspectJ註解切面完全一致。這種AOP風格的好處在於 能夠不使用XML來完成功能。

Spring在運行時通知對象

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

直到應用需要被代理的bean時,Spring才創建代理對象。如果使用的是ApplicationContext的話,在ApplicationContext從BeanFactory中加載所有bean的時候,Spring纔會創建被代理的對象。因爲Spring運行時才創建代理對象,所以我們不需要特殊的編譯器來織入Spring AOP的切面。

總而言之,Spring的切面由包裹了目標對象的代理類實現。 代理類處理方法的調用,執行額外的切面邏輯,並調用目標方法。

因爲Spring基於動態代理,因此Spring只支持方法級別的連接點。

2 通過切點來選擇連接點

切點用於準確定位應該在什麼地方應用切面的通知。通知和切點是切面的最基本元素。

關於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指示器是我們在編寫切點定義時最主要使用的指示器。在此基礎上,我們使用其他指示器來限制所匹配的切點。

2.1 編寫切點

爲了闡述Spring中的切面,我們需要有個主題來定義切面的切點。爲此,我們定義一 個Performance接口:

public interface Performance {
    public void perform();
}

Performance可以代表任何類型的現場表演,如舞臺劇、電影或音樂會。假設我們想編寫Performance的perform()方法觸發的通知。如下的表達式能夠設置當perform()方法執行時觸發通知的調用:

execution(* com.zjx.aspectj.Performance.perform(..))

解析:使用execution()指示器選擇Performance的perform()方法執行時觸發通知。方法表達式以“*”開始表示返回任意類型,也就是我們不關心方法返回值的類型;然後,指定全限定類名和方法名。對於方法參數列表(…)表示切點要選擇任意的perform()方法,無論入參是什麼。

假設我們要匹配的切點僅僅是com包,在此情景下,可以使用within()指示器來限定匹配:

execution(* com.zjx.aspectj.Performance.perform(..) && within(com.*))

這裏使用了”&&“操作符把execution()和within()指示器連接在一起,形成與(and)關係(切點必須匹配所有的指示器),還可以使用‘||’操作符來標識或(or)關係,使用‘!’操作符來標識非(not)操作。

2.2 在切點中選擇bean

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

execution(* com.zjx.aspectj.Performance.perform(..) and bean('woodstock')

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

在某些場景下,限定切點爲指定的bean或許很有意義,但我們還可以使用非操作爲除了特定ID 以外的其他bean應用通知:

execution(* com.zjx.aspectj.Performance.perform(..) and !bean('woodstock')

在此場景下,切面的通知會被編織到所有ID不爲woodstock的bean中。

3 使用註解創建切面

3.1 定義切面

@AspectJ:表明該類不僅僅是一個POJO,還 是一個切面。

AspectJ提供了五個 註解來定義通知

注  解 通  知
@After 通知方法會在目標方法返回或拋出異常後調用
@AfterReturning 通知方法會在目標方法返回後調用
@AfterThrowing 通知方法會在目標方法拋出異常後調用
@Around 通知方法會將目標方法封裝起來
@Before 通知方法會在目標方法調用之前執行

如下定義了一個切面,Audience有四個方法,定義了一個觀衆在觀看演出時可能會做的事情。在演出之前,觀衆要就坐(takeSeats())並將手機調至靜音狀態(silenceCellPhones())。如果演出很精彩的話,觀衆應該會鼓掌喝彩(applause())。不過,如果演出沒有達到觀衆預期的話,觀衆會 要求退款(demandRefund())。

package com.zjx.aspectj;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

/**
 * Created by zjx on 2018/6/15.
 */
@Aspect
public class Audience {

    //表演之前手機靜音
    @Before("execution(* com.zjx.aspectj.Performance.perform(..))")
    public void silenceCellPhones(){
        System.out.println("silencing cell phones");
    }

    //表演之前就做
    @Before("execution(* com.zjx.aspectj.Performance.perform(..))")
    public void takeSeats(){
        System.out.println("Taking seats");
    }

    //表演成功之後鼓掌
    @AfterReturning("execution(* com.zjx.aspectj.Performance.perform(..)))")
    public void applause(){
        System.out.println("CLAP CLAP CLAP!");
    }

    //表演失敗則要求退錢
    @AfterThrowing("execution(* com.zjx.aspectj.Performance.perform(..)))")
    public void demandRefund(){
        System.out.println("Demanding refund");
    }
    
}

Audience使用到了前面五個註解中的三個。takeSeats()和silence CellPhones()方 法都用到了@Before註解,表明它們應該在演出開始之前調用。applause()方法使用了@AfterReturning註解,它會在演出成功返回後調用。demandRefund()方法上添加了@AfterThrowing註解,這表明它會在拋出異常以後執行。

所有的這些註解都給定了一個切點表達式作爲它的值,同時,這四個方法的切點表達式都是相同的。可以使用 @Pointcut 註解在一個@AspectJ切面內定義可重用的切點,如下所示:

package com.zjx.aspectj;

import org.aspectj.lang.annotation.*;

/**
 * Created by zjx on 2018/6/15.
 */
@Aspect
public class AudienceUsePointCut {

    //定義切點表達式
    @Pointcut("execution(* com.zjx.aspectj.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 refund");
    }

}

此時切面創建成功了的,但是由於沒有啓用自動代理,即便使用了AspectJ註解, 但它並不會被視爲切面,這些註解不會解析,也不會創建將其轉換爲切面的代理。

有兩者方式可以啓動自動代理:

  1. JavaConfig方式:在配置類的類級別上通過EnableAspectJAutoProxy註解啓用自動代理功能。如下所示:
@EnableAspectJAutoProxy //啓用AspectJ自動代理
@Configuration //聲明一個配置類
@ComponentScan //啓用Spring自動掃描
public class ConcertConfig {
    
    //聲明Audience bean
    @Bean
    public Audience audience(){
        return new Audience();
    }

}
  1. 使用XML來裝配bean:使用Spring aop命名空間中的 aop:aspectj-autoProxy 元素啓動自動代理功能。如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 定義要掃描的包 -->
    <context:component-scan base-package="com.zjx.aspectj"></context:component-scan>
    <!-- 啓用自動代理 -->
    <aop:aspectj-autoproxy/>
    <!-- 聲明audience bean -->
    <bean id="audience" class="com.zjx.aspectj.Audience"/>
</beans>

不管使用哪種方式,AspectJ自動代理都會爲使用@AspectJ註解的bean創建一個代理,這個代理會圍繞着所有該切面的切點所匹配的bean。

在這種情況下,將會爲Concert bean創建一個代理,Audience類中的通知方法將會在perform()調用前後執行。

使用Spring的AspectJ自動代理創建的切面在本質上依然是Spring基於代理的切面。因此,想要利用AspectJ的所有能力,必須在運行時使用AspectJ並且不依賴Spring來創建基於代理的切面。

3.2 創建環繞通知

環繞通知是最爲強大的通知類型。它能夠讓你編寫的邏輯將被通知的方法完全包裹起來。實際上就像在一個通知方法中同時編寫前置通知和後置通知。

如下所示重寫了Audience類,它實現的功能和之前一樣,但是把四個方法寫在一個方法裏面了:

package com.zjx.aspectj;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

/**
 * Created by zjx on 2018/6/15.
 */
@Aspect
public class AudienceUseAround {

    @Pointcut("execution(* com.zjx.aspectj.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 (Throwable e) {
            //表演失敗則要求退錢
            System.out.println("Demanding refund");
        }

    }

}

它接受了ProceedingJoinPoint作爲參數,這個參數是必須要有的,因爲你需要在通知中通過它來調用被通知的方法通知方法中可以做任何的事情,當要將控制權交給被通知的方法時,它需要調用
ProceedingJoinPoint的 proceed()方法。

需要注意的是,別忘記調用proceed()方法。如果不調這個方法的話,那麼你的通知實際上會阻塞對被通知方法的調用。當然,也可以不調用該方法,從而阻塞對被通知方法的訪問。

3.3 處理通知中的參數

切面還能夠訪問和使用傳遞給被通知方法的參數,如下所示:

  @Pointcut("execution(* com.zjx.aspectj.Performance.perform(int) & args(number))")
  public void performance(int number){}
    
  @Before("performance(number)")
  public void silenceCellPhones(int number){
       ...
    }

    

切點表達式中的args(number)限定符。它表明傳遞給 perform()方法的int類型參數也會傳遞到通知中去。參數的名稱number也與切點方法簽名中的參數相匹配。

這個參數會傳遞到通知方法中,這個通知方法是通過@Before註解和命名切點 trackPlayed(trackNumber)定義的。切點定義中的參數與切點方法中的參數名稱是一樣的,這樣就完成了從命名切點到通知方法的參數轉移。

3.4 通過註解引入新功能

使用Spring AOP,我們可以爲bean引入新的方法。 代理攔截調用並委託給實現該方法的其他對象

當引入接口的方法被調用時,代理會把此調用委託給實現了新接口的某個其他對象。實際上,一個bean的實現被拆分到了多個類中。

爲了驗證該主意能行得通,我們爲示例中的所有的Performance實現引入下面的 Encoreable接口:

package com.zjx.aspectj;

/**
 * Created by zjx on 2018/6/20.
 */
public interface Encoreable {
    void performance();
}

我們需要有一種方式將這個接口應用到Performance實現中,藉助於AOP的引入功能,我們可以不必在設計上妥協或者侵入性地改變現有的實現。爲了實現該功能,我們要創建一個新的切面:

package com.zjx.aspectj;

/**
 * Created by zjx on 2018/6/20.
 */

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;

@Aspect
public class EncoreableIntroducer {

    @DeclareParents(value = "com.zjx.aspectj.Performance+",
                    defaultImpl = DefaultEncoreable.class)
    public static Encoreable encoreable;
}

可以看到,EncoreableIntroducer是一個切面。但是,它與我們之前所創建的切面不同,它並沒有提供前置、後置或環繞通知,而是通過 @DeclareParents註解,將Encoreable接口引入到Performance bean中。

@DeclareParents註解由三部分組成:

  • value屬性指定了哪種類型的bean要引入該接口。在本例中,也就是所有實現 Performance的類型。(標記符後面的加號表示是Performance的所有子類型,而不 是Performance本身。)

  • defaultImpl屬性指定了爲引入功能提供實現的類。在這裏,我們指定的 是DefaultEncoreable提供實現。

  • @DeclareParents註解所標註的靜態屬性指明瞭要引入了接口。在這裏,我們所引入 的是Encoreable接口。

和其他的切面一樣,我們需要在Spring應用中將EncoreableIntroducer聲明爲一個bean:

 <bean class="com.zjx.aspectj.EncoreableIntroducer"></bean>

4. 在XML中聲明切面

在Spring的aop命名空間中,提供了多個元素用來在XML中聲明切面,如下所示:

AOP配置元素 用途
aop:advisor 定義AOP通知器
aop:after 定義AOP後置通知(不管被通知的方法是否執行成功)
aop:after-returning 定義AOP返回通知
aop:after-throwimg 定義異常通知
aop:around 定義環繞通知
aop:aspect 定義一個切面
aop:aspect-autoproxy 啓用@Asepct註解驅動的切面
aop:before 定義一個前置通知
aop:config 頂層的AOP配置元素。大多數的aop:*元素必須包含在aop:config元素內
aop:decalre-parents 以透明的方式爲被通知的對象引入額外的接口
aop:pointcut 定義一個切點

我們已經看過了aop:aspectj-autoproxy元素,它能夠自動代理AspectJ註解的通知類。aop命名空間的其他元素能夠讓我們直接在Spring配置中聲明切面,而不需要使用註解。

4.1 聲明前置通知和後置通知

我們重新看一下Audience類,這一次我們將它所有的AspectJ註解全部移除掉:

public class AudienceUseXml {
    //表演之前手機靜音
    public void silenceCellPhones(){
        System.out.println("silencing cell phones");
    }

    //表演之前就做
    public void takeSeats(){
        System.out.println("Taking seats");
    }

    //表演成功之後鼓掌
    public void applause(){
        System.out.println("CLAP CLAP CLAP!");
    }

    //表演失敗則要求退錢
    public void demandRefund(){
        System.out.println("Demanding refund");
    }
}

我們可以使用Spring aop 命名空間中的一些元素,將沒有註解的Audience類轉換爲切面:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="audience" class="com.zjx.aspectj.AudienceUseXml"></bean>
    <aop:config>
        <!--引用audience bean -->
        <aop:aspect ref="audience">
            <!-- 定義前置通知,表演之前手機靜音-->
            <aop:before pointcut="execution(* com.zjx.aspectj.Performance.perform(..))" method="silenceCellPhones"/>

            <!-- 定義前置通知,表演之前就坐-->
            <aop:before method="takeSeats" pointcut="execution(* com.zjx.aspectj.Performance.perform(..))"/>

            <!-- 定義返回通知,表演成功之後鼓掌-->
            <aop:after-returning method="applause" pointcut="execution(* com.zjx.aspectj.Performance.perform(..))"/>

            <!-- 定義異常通知,表演失敗之後退錢-->
            <aop:after-throwing method="demandRefund" pointcut="execution(* com.zjx.aspectj.Performance.perform(..))"/>
        </aop:aspect>

    </aop:config>
</beans>

關於Spring AOP配置元素,第一個需要注意的是大多數AOP配置元素必須在aop:config元素的上下文應用。這條規則有幾個例外的場景,但是把一個bean聲明爲一個切面時我們總是從aop:config中配置的。

aop:config元素內,我們可以聲明一個或多個通知器、切面或者切點。

ref元素引用了一個POJO bean,該bean實現了切面的功能,提供了在切面中通知所調用的方法。

在所有的通知元素中,pointcut屬性定義了通知所應用的切點,它的值是使用AspectJ切點表達式語法所定義的切點。

使用aop:pointcut元素將通用的切點表達式抽取到一個切點聲明中:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="audience" class="com.zjx.aspectj.AudienceUseXml"></bean>
    <aop:config>
        <!--引用audience bean -->
        <aop:aspect ref="audience">

            <!-- 定義通用切點 -->
            <aop:pointcut id="performance" expression="execution(* com.zjx.aspectj.Performance.perform(..))"/>

            <!-- 定義前置通知,表演之前手機靜音-->
            <aop:before pointcut-ref="performance"  method="silenceCellPhones"/>

            <!-- 定義前置通知,表演之前就坐-->
            <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>
</beans>

現在切點是在一個地方定義的,並且被多個通知元素所引用。aop:pointcut元素定義了一個id爲performance的切點。同時修改所有的通知元素,用pointcut-ref屬性來引用這個命名切點。

如果想讓定義的切點能夠在多個切面使用,我們可以把aop:pointcut元素放在aop:config元素的範圍內。

4.2 聲明環繞通知

使用環繞通知,我們可以完成 前置通知和後置通知所實現的相同功能,而且只需要在一個方法中 實現。如下所示watchPerformance()方法提供了AOP環繞通知:

public class AudienceUseAroundXml {

    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 (Throwable e) {
            //表演失敗則要求退錢
            System.out.println("Demanding refund");
        }
    }

}

聲明環繞通知與聲明其他類型的通知並沒有太大區別。我們所需要做的僅僅是使 用aop:around元素。

    <bean id="audience" class="com.zjx.aspectj.AudienceUseAroundXml"></bean>
    <aop:config>
        <!--引用audience bean -->
        <aop:aspect ref="audience">

            <!-- 定義通用切點 -->
            <aop:pointcut id="performance" expression="execution(* com.zjx.aspectj.Performance.perform(..))"/>

            <!-- 定義前置通知,表演之前手機靜音-->
            <aop:around pointcut-ref="performance"  method="watchPerformance"/>

        </aop:aspect>

    </aop:config>
4.3 爲通知傳遞參數
    <bean id="audience" class="com.zjx.aspectj.Audience"></bean>
    
    <aop:config>
        <!--引用audience bean -->
        <aop:aspect ref="audience">

            <!-- 定義通用切點 -->
            <aop:pointcut id="performance" expression="execution(* com.zjx.aspectj.Performance.perform(int) 
            and args(number)"/>

            <!-- 定義前置通知,表演之前手機靜音-->
            <aop:before pointcut-ref="performance"  method="silenceCellPhones"/>

        </aop:aspect>

    </aop:config>

我們使用了和前面相同的aop命名空間XML元素,它們會將POJO聲明爲切面。唯一明顯的差別在於切點表達式中包含了一個參數,這個參數會傳遞到通知方法中。這個表達式與之前通過Java配置的表達式幾乎是相同的。唯一的差別在於這裏使用and關鍵字而不是“&&”(因爲在XML中,“&”符號會被解析爲實體的開始)。

4.4 通過切面引入新的功能

使用Spring aop命名空間中的aop:declare-parents元素,我們可以實現與@DeclareParents註解相同的功能。

    <aop:config>
        <!--引用audience bean -->
        <aop:aspect >
            <aop:declare-parents
                    types-matching="com.zjx.aspectj.Performance+"
                    implement-interface="com.zjx.aspectj.Encoreable"
                    default-impl="com.zjx.aspectj.DefaultEncoreable"/>
        </aop:aspect>

    </aop:config>

aop:declare-parents元素聲明瞭此切面所通知的bean要在它的對象層次結構中擁有新的父類型。

types-matching屬性指定要通知的bean,+表示該接口的實現類

implement-interface屬性指定bean要增加的父類接口

default-impl屬性指定父類接口的實現類(也可以使用delegate-ref屬性來標識)。

5. 注入AspectJ切面

AspectJ提供了Spring AOP所不能支持的許多類型的切點。

如果在執行通知時,切面依賴於一個或多個類,我們可以在切面內部實例化這些協作的對象。但更好的方式是,我們可以藉助Spring的依賴注入把bean裝配進AspectJ切面中。

爲了演示,我們爲上面的演出創建一個新切面。具體來講,我們以切面的方式創建一個評論員的角色,他會觀看演出並且會在演出之後提供一些批評意見。下面的CriticAspect就是一個這樣的切面。

public aspect CriticAspect {    
    private Worker worker;

    public Audience(){}

    //通過setter方法注入
    public void setWorker(Worker worker){
        this.worker = worker;
        System.out.println("工作人員已入場");
    }

    //定義piano構造器切點和後置通知
    pointcut piano():execution(concert.PianoPerform.new());
    after():piano(){
        worker.sendMsg("鋼琴");
    }


    //定義不帶參數方法切點和前置通知
    pointcut perform():execution(* concert.Performance.perform());
    before():perform(){
        worker.take();
    }

    //定義帶兩個參數的切點和後置通知
    pointcut finishPerform(String performer, String title):execution(* concert.Performance.finishPerform(String, String)) && args(performer, title);
    after(String performer, String title):finishPerform(performer, title){
        worker.broadcast(performer, title);
    }
}

AspectJ切面根本不需要Spring就可以織入到我們的 應用中。如果想使用Spring的依賴注入爲AspectJ切面注入協作者,那我們就需要在Spring配置 中把切面聲明爲一個Spring配置中的。如下的聲明會把 criticismEnginebean注入到CriticAspect中:

很大程度上,的聲明與我們在Spring中所看到的其他配置並沒有太多的區別,但是最大的不同在於使用了factory-method屬性。通常情況下,Spring bean由Spring容器初始化,但是AspectJ切面是由AspectJ在運行期創建的。等到Spring有機會爲CriticAspect注入CriticismEngine時,CriticAspect已經被實例化了。

因爲Spring不能負責創建CriticAspect,那就不能在Spring中簡單地把CriticAspect聲明爲一個bean。相反,我們需要一種方式爲Spring獲得已經由AspectJ創建的CriticAspect實例的句柄,從而可以注入CriticismEngine。幸好,所有的AspectJ切面都提供了一個靜態的 aspectOf()方法,該方法返回切面的一個單例。所以爲了獲得切面的實例,我們必須使用factory-method來調用asepctOf()方法而不是調用CriticAspect的構造器方法。

簡而言之,Spring不能像之前那樣使用聲明來創建一個CriticAspect實例——它已經在運行時由AspectJ創建完成了。Spring需要通過aspectOf()工廠方法獲得切面的引用,然後像元素規定的那樣在該對象上執行依賴注入。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章