4.Spring學習筆記之面向切面的Spring

在軟件開發中,散佈於應用中多處的功能被稱爲橫切關注點。把這些橫切關注點與業務邏輯相分離正是面向切面編程(AOP)所要解決的問題。
依賴注入(DI)有助於應用對象之間的解耦,而AOP可以實現橫切關注點與它們所影響的對象之間的解耦。

1.什麼是面向切面編程

在使用面向切面編程時,我們仍然在一個地方定義通用功能,但是可以通過聲明的方式定義這個功能要以何種方式在何處應用,而無需修改受影響的類。
橫切關注點可以被模塊化爲特殊的類,這些類被稱爲切面(aspect)。
描述切面的常用術語有通知(advice)、切點(pointcut)、連接點(join point)
在AOP術語中,切面的工作被稱爲通知。通知定義了切面是什麼以及何時使用。除了描述切面要完成的工作,通知還解決了何時執行這個工作的問題。
Spring切面可以應用5種類型的通知:

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

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

切點(Poincut)
如果說通知定義了切面的“什麼”和“何時”的話,那麼切點就定義了“何處”。切點的定義會匹配所要織入的一個或多個連接點。我們通常使用明明確的類和方法名稱,或是利用正則表達式定義所匹配的類和方法名稱來指定這些切點。

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

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

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

  • 編譯期:切面在目標類編譯時被織入。這種方式需要特殊的編譯器。AspectJ的織入編譯器就是一這種方法織入切面的。
  • 類加載期:切面在目標類加載到JVM時被織入。這種方式需要特殊的類加載器(ClassLoader),它可以在目標類被引入應用之前增強該目標了IDE字節碼。AspectJ 5的加載時織入就支持以這種方式織入切面。
  • 運行期:切面在應用運行的某個時刻被織入。一般情況下,在織入切面時,AOP容器會爲目標對象動態創建一個代理對象。Spring AOP就是以這種方式織入切面。

Spring對AOP的支持
創建切點來定義切面所織入的連接點是AOP框架的基本功能。
Spring提供了4中類型的AOP支持:

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

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

Spring所創建的通知都是用標準的Java類編寫的。

通過在代理類中包裹切面,Spring在運行期把切面織入到Spring管理的bean中。Spring的切面包裹了目標對象的代理類實現。代理類處理方法的調用,執行額外的切面邏輯,並調用目標方法。直到應用需要被代理的bean時,Spring才創建代理對象。因爲Spring運行時才創建代理對象,因此我們不需要特殊的編譯器來織入Spring AOP的切面。

Spring只支持方法級別的連接點。

2.通過切點來選擇連接點

在Spring AOP中,要使用AspectJ的切點表達式語言來定義切點。關於Spring AOP的AspectJ切點,最重要的一點就是Spring僅支持AspectJ切點指示器的一個子集。因爲Spring是基於代理的,而某些切點表達式與基於代理的AOP無關。
下圖爲Spring AOP所支持的AspectJ切點指示器:
Spring藉助AspectJ的切點表達式語言來定義Spring切面
execution指示器執行匹配,其他指示器來限制匹配的切點。
除此之外,Spring還引入一個新的bean()指示器,它允許我們在切點表達式中使用bean的ID來標示bean。
例如,我們要使用AspectJ切點表達式來選擇一個名爲Performance類中的perform()方法:

execution(* concert.Perfoemance.perform(..))
/**我們使用execution()指示器來選擇Performance的perform()方法。方法表達式以“ * ”號開始,表明我們不關心方法返回值的類型。然後,指定了全限定類名和方法名。對於參數列表,我們使用兩個點號(..)表明切點要選擇任意的perform()方法,無論該方法的入參是什麼。**/
/**如果我們需要配置的切點僅匹配concert包,我們可以使用within()指示器來限制。**/
execution(* concert.Perfoemance.perform(..)) && within(concert.*)

因爲“&”在XML中有特殊含義,所以Spring的XML配置裏面描述切點時,我們可以使用and、or、not來代替“&&”、“||”、“!”。

execution(* concert.Performance.perform(..)) and bean('woodstock')

3.使用註解創建切面

如果我們把Performance類中perform()方法看成是一個表演的話,那麼下面這個切面定義的是在表演之前、表演之後、以及表演失敗之後觀衆的反映(即程序的輸出)。

@Aspect //表明Audience是一個切面
public class Audience {

    //表演之前
    @Before("execution(** concert.Performance.perform(..))")
    public void silenceCellPhone() {
        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("要求退款");
    }
}

此外,@Pointcut註解能夠在一個@Aspect切面內定義可重用的切點

@Aspect //表明Audience是一個切面
public class Audience {

    //定義命名的切點
    @Pointcut("execution(** concert.Performance.perform(..))")
    public void performance() {}

    //表演之前
    @Before("performance()")
    public void silenceCellPhone() {
        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("要求退款");
    }
}

在這裏,Audience仍然是一個Java類,只不過它通過註解表明會作爲切面使用而已。同其他類一樣,可以裝配爲Spring中bean。

除此之外,你還需要對AspectJ註解進行配置,不然這些代碼不會生效。如果使用JavaConfig的話,可以在配置類的類級別上通過使用@EnableAspectJAutoProxy註解啓用自動代理:

@Configuration
@EnableAspectJAutoProxy //啓用AspectJ自動代理
@Component
public class ConcertConfg{
    //聲明Audience bean
    @Bean
    public Audience audience() {
        return new Audience();
    }
}

如果使用xml裝配bean的話,那麼需要使用Spring aop命名空間的<aop:aspectj-autoproxy>元素:

<beans
    ...
    記得加命名空間哦>
    <context:component-scan base-package="concert" />
    <aop:aspectj-autoproxy>
    <bean class="concert.Audience" />
</beans>

需要記住的是,Spring的AspectJ自動代理僅僅使用@AspectJ作爲創建切面的指導,切面依然是基於代理的。在本質上,它依然是Spring基於代理的切面。這一點非常重要,因爲這意味着儘管使用的是@AspectJ註解,但我們仍然限於代理方法的調用。如果想利用AspectJ的所有能力,我們必須在運行時使用AspectJ並且不依賴Spring來創建基於代理的切面。

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


@Aspect //表明Audience是一個切面
public class Audience {

    //定義命名的切點
    @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("要求退款");
        }
    }

}

ProceedingJoinPoint 對象參數是必須的。通知中通過他來調用被通知的方法。

處理通知中的參數

@Aspect //表明Audience是一個切面
public class Audience {

    //定義命名的切點
    @Pointcut("execution(** concert.Performance.perform(String))"
            +"&& args(songName)")
    public void performance(String songName) {}

    @Before("performance(songName)")
    public void watchPerformance(String songName) {

            System.out.println("演唱的歌曲是"+songName);
    }

}

這樣就可以將方法中的參數傳達到通知中。

通過註解引入新功能

當引入接口的方法被調用時,代理會把此調用委託給實現了新接口的某個其他對象。也就是說,一個bean的實現被拆分到多個類中。
爲了實現該功能,我們需要創建一個新的切面:

@Aspect 
public class EncoreableIntroducer {

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

通過@DeclareParents註解,將Encoreable 接口引入到Performance bean中。
@DeclareParents註解由三部分組成:

  • value屬性指定了哪種類型的bean要引入該接口。在本例中,也就是所有顯示Performance的類型。(標記符後面的加號表示是Performance的所有子類型,而不是Performance本身。)
  • defaultImpl屬性指定了爲引入功能提供實現的類。在這裏,我們指定的是DefaultEncoreable提供實現。
  • @DeclareParents註解所標註的靜態屬性指明瞭要引入的接口。在這裏,我們所引入的是Encoreable 接口。

4.在XML中聲明切面

在Spring的aop命名空間中,提供了多個元素用來在XML中聲明切面
這裏寫圖片描述
這裏寫圖片描述

去掉Audience所有的Aspect註解

public class Audience {

    public void silenceCellPhone() {
        System.out.println("將手機調至靜音狀態");
    }

    public void takeSeats() {
        System.out.println("就坐");
    }

    public void applause() {
        System.out.println("鼓掌喝彩");
    }

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

聲明前置通知和後置通知

<aop:config>
    <!-- 引用audience Bean -->
    <aop:aspect ref="audience">
        <aop:before 
            pointcut="execution(** concert.Performance.perform(..))"
            method="silenceCellPhone" />

        <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>

使用<aop:pointcut>定義切點

<aop:config>
    <!-- 引用audience Bean -->
    <aop:aspect ref="audience">
        <!-- 定義切點 -->
        <aop:pointcut
        id="performance"
        expression="execution(** concert.Performance.perform(..))" />

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

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

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

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

聲明環繞通知

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:config>
    <!-- 引用audience Bean -->
    <aop:aspect ref="audience">
        <!-- 定義切點 -->
        <aop:pointcut
        id="performance"
        expression="execution(** concert.Performance.perform(..))" />
        <!-- 聲明環繞通知 -->
        <aop:around 
            pointcut-ref="performance"
            method="watchPerformance" />

    </aop:aspect>
</aop:config>

爲通知傳遞參數

public class Audience {
    //要聲明爲前置通知的方法
    public void watchPerformance(String songName) {
            System.out.println("演唱的歌曲是"+songName);
    }
}
<aop:config>
    <!-- 引用audience Bean -->
    <aop:aspect ref="audience">
        <!-- 定義切點 -->
        <aop:pointcut
        id="performance"
        expression="execution(** concert.Performance.perform(String))
        and args(songName)" />

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

    </aop:aspect>
</aop:config>

通過切面引入新的功能

<aop:aspect>
<aop:declare-parents
    types-matching="concert.Performance+"
    implement-interface="concert.Encoreable"
    default-impl="concert.DefaultEncoteable"
/>
</aop:aspect>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章