第四章 面向切面的Spring

第四章 面向切面的Spring

[TOC]

面向切面要解決的問題

在軟件開發(fā)中故响,散布于應(yīng)用中多處的功能被稱為 橫切關(guān)注點宏蛉,例如事務(wù)、安全庭砍、日志场晶、權(quán)限控制………通常來講,這些橫切關(guān)注點從概念上是與業(yè)務(wù)的應(yīng)用邏輯相分離的怠缸。把這些橫切關(guān)注點與業(yè)務(wù)邏輯相分離正是面向切面編程所要解決的問題诗轻。

橫切關(guān)注點可以被模塊化為特殊的類,這些類被稱為切面

面向切面常用術(shù)語

描述切面的常用術(shù)語有 :通知揭北、切點和連接點 概耻。

通知、切點和連接點

通知

在AOP術(shù)語中罐呼,切面(橫切關(guān)注點可以被模塊化為特殊的類)的工作被稱為通知鞠柄。通知定義了 切面是什么以及何時使用,除了描述切面所完成的工作嫉柴,通知還解決了何時執(zhí)行這個工作的問題厌杜。

Spring切面可以應(yīng)用五種類型的通知 :

  • 前置通知 :在目標方法被 調(diào)用之前 調(diào)用通知功能
  • 后置通知 :在目標方法 完成之后 調(diào)用通知,不會關(guān)心方法的輸出是什么
  • 返回通知 :在目標方法 成功執(zhí)行 之后調(diào)用通知
  • 異常通知 :在目標方法 拋出異常 后調(diào)用通知
  • 環(huán)繞通知 :通知包裹了被通知的方法计螺,在目標方法 調(diào)用之前和調(diào)用之后 執(zhí)行自定義的行為

連接點

連接點就是在 應(yīng)用執(zhí)行過程中 能夠 插入切面 的一個點

切點

一個切面不需要通知應(yīng)用的所有連接點夯尽。切點有助于縮小切面所通知的連接點。如果說通知定義了切點是什么以及切點在何時使用的話登馒,那么切點就定義了“何處”匙握。切點的定義會匹配通知所要 織入 的一個或者多個連接點。

切面

切面是通知和切點的結(jié)合陈轿。通知和切點共同定義了切面的全部內(nèi)容——它是什么圈纺,在何時和何處完成其功能秦忿。

引入

引入允許我們向現(xiàn)有類添加新方法或者屬性。在無需修改現(xiàn)有的類的情況下蛾娶,讓他們具有新的行為和狀態(tài)灯谣。

織入

織入是把切面應(yīng)用到目標對象并創(chuàng)建新的代理的過程。切面在指定的連接點被織入到目標對象中蛔琅。在目標對象的生命周期里有多個點可以進行織入 :

  • 編譯期 : 切面在目標類編譯時被織入胎许。這種方式需要有特殊的編譯器。
  • 類加載期 :切面在目標類加載到JVM時被織入罗售。這種方式需要有特殊的類加載器辜窑。
  • 運行期 :切面在應(yīng)用運行的某個某個時刻被織入。一般情況下寨躁,在織入切面時谬擦,AOP容器會為目標對象動態(tài)地創(chuàng)建一個代理對象。Spring AOP就是以這種方式注入切面的朽缎。

總結(jié)

通知包含了需要用于多個應(yīng)用對象的橫切行為(橫切關(guān)注點);連接點是程序執(zhí)行過程中能夠應(yīng)用通知的所有點谜悟;切點定義了通知被應(yīng)用的具體位置(在哪些連接點會得到通知)话肖。

Spring對AOP的支持

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

  • [x] 基于代理的經(jīng)典Spring AOP
  • [ ] 純POJO切面(借助aop命名空間可以將POJO轉(zhuǎn)換為切面,需要XML配置)
  • [ ] @AspectJ注解驅(qū)動的切面(不使用XML來完成功能)
  • [ ] 注入式AspectJ切面(如果AOP需求超過了簡單的方法調(diào)用如構(gòu)造器或者屬性攔截葡幸,那么你需要考慮使用AspectJ來實現(xiàn)切面最筒,適用于Spring各版本)

Spring通知是使用Java編寫的,定義通知所應(yīng)用的切點通常會使用注解或XML編寫蔚叨。

通過在代理類中包裹切面(通知 + 切點)床蜘,Spring在運行期把切面織入到Spring管理的bean中。如圖蔑水,代理類封裝了目標類邢锯,并攔截被通知方法的調(diào)用,再把調(diào)用轉(zhuǎn)發(fā)給真正的目標bean搀别。當代理攔截到方法調(diào)用時丹擎,在調(diào)用目標bean方法之前,會執(zhí)行切面邏輯歇父。

SPring的切面由包裹了目標對象的代理類實現(xiàn)蒂培。代理類處理方法的調(diào)用,執(zhí)行額外的切面邏輯榜苫,并調(diào)用目標方法

直到應(yīng)用需要被代理的bean時护戳,Spring才會創(chuàng)建代理對象。

因為Spring基于動態(tài)代理垂睬,所以Spring只支持方法連接點媳荒。Spring缺少對字段連接點的支持抗悍,無法讓我們創(chuàng)建細粒度的通知,例如攔截對象字段的修改肺樟。

通過切點來選擇連接點

使用AspectJ的切點表達式語言

在Spring AOP中檐春,使用AspectJ的切點表達式語言定義切點,其中execution是最重要的描述符 :

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
            throws-pattern?)

除了返回類型么伯、方法名稱以及參數(shù)列表之外疟暖,其余都是可選的(即含有?的都是可選的)

  • modifiers-pattern? :方法修飾符(public、private田柔、默認俐巴、protected)
  • ret-type-pattern :方法返回類型
  • declaring-type-pattern? :方法類所在路徑
  • name-pattern :方法名
  • param-pattern :方法參數(shù)類型,可有一個或者多個硬爆,用(..)表示零個或者任意個參數(shù)欣舵,多個參數(shù)還可以用,分隔,也可以用(*)表示匹配任意類型的參數(shù)
  • throws-pattern? :表示方法拋出的異常

例如 :

匹配所有的public方法 :

execution(public * *(..))

匹配所有方法名開頭為set的 :

execution(* set*(...))

匹配定義在AccountService接口類中的所有方法

execution(* com.xyz.service.AccountService.*(...))

匹配定義在service包下的所有方法

execution(* com.xyz.service.*.*(..))

匹配定義在service包或者子包下的所有方法

execution(* com.xyz.service..*.*(..))
Aspect指示器 描述
execution() 用于匹配連接點
arg() 表明連接點參數(shù)類型是匹配類
@args() 表明參數(shù)注解是匹配類
this() 匹配一個bean缀磕,這個bean是一個指定類型的實例
target 匹配一個目標對象缘圈,此對象是一個給定類型的實例
@target() 匹配對象類需要有指定類型的注解
within() 限制連接點匹配指定的類型
@within() 匹配方法,該方法需要給定一個特定注解
@annotation 匹配帶有指定注解的連接點

編寫切點

假設(shè)我們需要編寫Performance類型 :

public interface Performance{
    public void perform();
}

我們想要編寫一個切面袜蚕,在調(diào)用Performance類中的perform方法時觸發(fā)通知 :

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

現(xiàn)在我們假設(shè)我們需要配置的切點僅僅匹配concert包 :

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

當使用Spring的XML來描述切面時候糟把,我們可以使用and來替換&&,同樣的牲剃,ornot可以替換||!

在切點中選擇bean

Spring中的bean()指示器允許我們在切點表達式中使用bean的ID來標識bean遣疯。bean()使用bean IDbean名稱作為參數(shù)來限制切點只匹配特定的bean

例如 :

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

在上面的例子中,我們希望在執(zhí)行perform()方法時應(yīng)用通知凿傅,但是限制bean的ID為woodstock

使用注解創(chuàng)建切面

定義切面

@Aspect
public class Audience {
    // 演出之前
    @Before("execution(public * concert.Performance.perform(..))")
    public void silenceCellPhone(){
        System.out.println("手機靜音");
    }
    // 演出之前
    @Before("execution(public * concert.Performance.perform(..))")
    public void takeSeats(){
        System.out.println("對號入座");
    }
    // 演出成功之后
    @AfterReturning("execution(public * concert.Performance.perform(..)")
    public void applause(){
        System.out.println("掌聲雷動");
    }
    // 演出失敗之后
    @AfterThrowing("execution(public * concert.Performance.perform(..))")
    public void demandRefund(){
        System.out.println("我想退款!");
    }

}

Audience類使用了@Aspect注解進行標注缠犀,表明該類不僅是一個POJO,還是一個切面聪舒。Audience類中的方法都是用注解來定義切面的具體行為辨液。

AspectJ使用了五個注解來定義通知 :

注解 通知
@After 通知方法在目標方法返回或者拋出異常時調(diào)用
@AfterReturning 通知方法在目標方法返回后調(diào)用
@AfterThrowing 通知方法在目標方法拋出異常后調(diào)用
@Around 通知方法會將目標方法封裝起來
@Before 通知方法在目標方法調(diào)用之前調(diào)用

在上面的例子中,我們定義了四個切點表達式箱残,這四個表達式完全可以進行整合 :

PointCut注解能夠在一個@Aspect切面內(nèi)定義可以重復(fù)的切點

@Aspect
public class Audience {
    @Pointcut("execution(public * concert.Performance.perform(..))")
    public void perform(){}
    // 演出之前
    @Before("perform()")
    public void silenceCellPhone(){
        System.out.println("手機靜音");
    }
    // 演出之前
    @Before("perform()")
    public void takeSeats(){
        System.out.println("對號入座");
    }
    // 演出成功之后
    @AfterReturning("perform()")
    public void applause(){
        System.out.println("掌聲雷動");
    }
    // 演出失敗之后
    @AfterThrowing("perform()")
    public void demandRefund(){
        System.out.println("我想退款!");
    }
}

我們還需要啟動AspectJ的自動代理 :

如果你使用JavaConfig注解的話室梅,你可以在配置類上加上@EnableAspectJAutoProxy注解啟動自動代理的功能

// 啟動AspectJ自動代理
@EnableAspectJAutoProxy
@Configuration
public class ConcertConfig {
    @Bean
    public Audience audience(){
        return new Audience();
    }
}

如果使用XML裝配的話,我們需要<aop:aspectj-autoproxy />啟動自動代理 :

<bean id="audience" class="concert.Audience"/>
<bean id="musicPerformance" class="concert.MusicPerformance"/>
<!-- 開啟自動代理 -->
<aop:aspectj-autoproxy/>

Spring的AspectJ自動代理僅僅使用@Aspect作為創(chuàng)建切面的指導(dǎo)疚宇,切面依然是基于代理的亡鼠。在本質(zhì)上,它依然是Spring基于代理的切面敷待。這一點非常重要间涵,因為這意味著盡管使用的是@Aspect注解,但是仍然限于代理方法的調(diào)用榜揖。如果想使用AspectJ的所有能力勾哩,我們必須在運行時使用AspectJ并且不依賴Spring來創(chuàng)建基于代理的切面抗蠢。

創(chuàng)建環(huán)繞通知

環(huán)繞通知能夠讓你所編寫的邏輯將被通知的目標方法(連接點)完全包裹起來。就像是在一個通知方法中同時編寫前置后置通知思劳。

@Aspect
public class Audience {

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

    // 環(huán)繞通知
    @Around("perform()")
    public void watchPerformance(ProceedingJoinPoint joinPoint){
        try{
            System.out.println("關(guān)閉手機");
            System.out.println("入座");
            // 通過ProceedingJoinPoint來調(diào)用被通知的方法
            joinPoint.proceed();
            System.out.println("掌聲雷動");
        }catch(Throwable e){
            System.out.println("我要退款");
        }
    }
}

@Around注解表明watchPerformance()方法會作為performance切點的環(huán)繞通知迅矛。當通知方法需要把控制權(quán)交給被通知方法時候,需要調(diào)用ProceedingJoinPointproceed()方法潜叛。如果不調(diào)用這個方法的話秽褒,你的通知會阻塞對被通知方法的調(diào)用。

為通知傳遞參數(shù)

BlankDisc中威兜,我們需要統(tǒng)計磁道被播放的數(shù)量 :


public class BlankDisc implements CompactDisc {

    private String title;
    private String artist;
    private List<String> tracks;
    public BlankDisc(String title,String artist,List<String> tracks) {
        this.title = title;
        this.artist = artist;
        this.tracks = tracks;
    }
    public String getTitle() {
        return title;
    }
    public String getArtist() {
        return artist;
    }
    public List<String> getTracks() {
        return tracks;
    }
    public void play() {
        System.out.println("title :" + title + " artist :" + artist);
        for (int trackNumber = 0;trackNumber < tracks.size();trackNumber ++){
            playTrack(trackNumber);
        }
    }
    public void playTrack(int trackNumber) {
        System.out.println("track "+ trackNumber + " : " + tracks.get(trackNumber));
    }
}

我們定義TrackCounter來描述切面 :

@Aspect
@Component
@EnableAspectJAutoProxy
public class TrackCounter {
    public Map<Integer,Integer> trackCounts = new HashMap<Integer, Integer>();
    @Pointcut("execution(public * soundsystem.BlankDisc.playTrack(int)) && args(trackNumber))")
    public void trackPlayed(int trackNumber){}
    @AfterReturning("trackPlayed(trackNumber)")
    public void countTrack(int trackNumber){
        trackCounts.put(trackNumber,getPlayCount(trackNumber) + 1);
        System.out.println("--->track " + trackNumber + "數(shù)量增加了.");
    }
    public int getPlayCount(int trackNumber){
        return trackCounts.containsKey(trackNumber)?
                trackCounts.get(trackNumber):0;
    }
}

切點表達式中的args(trackNumber)表明 :傳遞給連接點的int類型的參數(shù)也會傳遞到通知方法中销斟。參數(shù)的名稱為trackNumber,與切點方法簽名中的參數(shù)相匹配椒舵。在@AfterReturing("trackNumber")表達式下面蚂踊,切點方法和切點定義的參數(shù)名一致。

通過注解引入新功能

TODO

在XML中聲明切面

前置后置通知

<!-- 切面配置 -->
<!-- 頂層的aop配置元素 -->    
<aop:config>
        <!-- 定制一個切面 -->
        <aop:aspect ref="audience">
            <!-- 定義一個切點 -->
            <aop:pointcut id="perform" expression="execution(public * concert.Performance.perform(..))"/>

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

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

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

            <aop:after-throwing method="demandRefund" pointcut-ref="perform"/>

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

環(huán)繞通知

<bean id="musicPerformance" class="concert.MusicPerformance"/>
<bean id="audience" class="concert.Audience"/>
<aop:config>
    <aop:aspect ref="audience">
    <aop:pointcut id="performance" expression="execution(public * concert.Performance.perform())"/>
    <aop:around method="execute" pointcut-ref="performance"/>
        </aop:aspect>
</aop:config>

為通知傳遞參數(shù)

<beans> 
    <bean id="blankDisc" class="soundsystem.BlankDisc"
          c:_0="${disc.title}" c:_1="${disc.artist}" c:_2-ref="blankDiscList"/>

    <bean id="cdPlayer" class="soundsystem.CDPlayer"/>

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

    <util:list id="blankDiscList">
        <value>老古董</value>
        <value>大千世界</value>
        <value>如約而至</value>
        <value>柳成蔭</value>
    </util:list>

    <context:property-placeholder location="classpath:/application.properties"/>

    <aop:aspectj-autoproxy/>

    <import resource="classpath:/aopconfig.xml"/>
</beans>
<beans>
    <aop:config>
        <aop:aspect ref="trackCounter">
        <aop:pointcut id="playTrack" expression="execution(* soundsystem.BlankDisc.playTrack(int)) and args(trackNumber)"/>
        <aop:after-returning pointcut-ref="playTrack" method="countTrack"/>
        </aop:aspect>
    </aop:config>
</beans>

通過切面引入新的功能

TODO

注入AspectJ切面

TODO

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末笔宿,一起剝皮案震驚了整個濱河市犁钟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌泼橘,老刑警劉巖涝动,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異侥加,居然都是意外死亡,警方通過查閱死者的電腦和手機粪躬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門担败,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人镰官,你說我怎么就攤上這事提前。” “怎么了泳唠?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵狈网,是天一觀的道長。 經(jīng)常有香客問我笨腥,道長拓哺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任脖母,我火速辦了婚禮士鸥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谆级。我一直安慰自己烤礁,他們只是感情好讼积,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著脚仔,像睡著了一般勤众。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鲤脏,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天们颜,我揣著相機與錄音,去河邊找鬼凑兰。 笑死掌桩,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的姑食。 我是一名探鬼主播波岛,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼音半!你這毒婦竟也來了则拷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤曹鸠,失蹤者是張志新(化名)和其女友劉穎煌茬,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體彻桃,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡坛善,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了邻眷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片眠屎。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖肆饶,靈堂內(nèi)的尸體忽然破棺而出改衩,到底是詐尸還是另有隱情,我是刑警寧澤驯镊,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布葫督,位于F島的核電站,受9級特大地震影響板惑,放射性物質(zhì)發(fā)生泄漏橄镜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一冯乘、第九天 我趴在偏房一處隱蔽的房頂上張望蛉鹿。 院中可真熱鬧,春花似錦往湿、人聲如沸妖异。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽他膳。三九已至响逢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間棕孙,已是汗流浹背舔亭。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蟀俊,地道東北人钦铺。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像肢预,于是被迫代替她去往敵國和親矛洞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容