【Spring實戰(zhàn)】面向切面編程

本章內(nèi)容:

  • 面向切面編程的基本原理
  • 通過POJO創(chuàng)建切面
  • 使用@AspectJ注解
  • 為AspectJ切面注入依賴

軟件系統(tǒng)中的一些功能需要用到應(yīng)用程序的多個地方鸽捻,但是我們又不想在每個點都明確調(diào)用它們说榆。日志渔隶、安全和事務(wù)管理的確都很重要,但它們不應(yīng)是應(yīng)用對象主動參與的行為铺呵,應(yīng)該要讓應(yīng)用對象只關(guān)注于自己所針對的業(yè)務(wù)領(lǐng)域問題裹驰,其他方面的問題由其他應(yīng)用對象來處理。

在軟件開發(fā)中片挂,散布于應(yīng)用中多處的功能被稱為橫切關(guān)注點(cross-cutting concern)幻林。通常,這些橫切關(guān)注點從概念上與應(yīng)用的業(yè)務(wù)邏輯分離的(但是往往會直接嵌入到應(yīng)用的業(yè)務(wù)邏輯之中)音念。把這些橫切關(guān)注點與業(yè)務(wù)邏輯相分離正是面向切面編程(AOP)所要解決的問題沪饺。

裝配Bean介紹了如何使用依賴注入(DI)管理和配置我們的應(yīng)用對象。DI有助于應(yīng)用對象之間的解耦闷愤,而AOP可以實現(xiàn)橫切關(guān)注點與它們所影響的對象之間的解耦整葡。

本章展示了Spring對切面的支持,包括如何把普通類聲明為一個切面和如何使用注解創(chuàng)建切面讥脐。除此之外遭居,還會看到AspectJ(另一種流行的AOP實現(xiàn))如何補充AOP框架的功能。

什么是面向切面編程

切面能幫助我們模塊化橫切關(guān)注點旬渠。橫切關(guān)注點可以被描述為影響應(yīng)用多處的功能俱萍。下圖呈現(xiàn)了橫切關(guān)注點的概念:

切面實現(xiàn)了橫切關(guān)注點(跨多個應(yīng)用對象的邏輯)的模塊化

上圖展現(xiàn)了一個被劃分為模塊的典型應(yīng)用。每個模塊的核心功能都是為特定業(yè)務(wù)領(lǐng)域提供服務(wù)告丢,但是這些模塊都需要類似的輔助功能枪蘑。

重用通用功能最常見的面向?qū)ο蠹夹g(shù)是繼承(inheritance)或委托(delegation)。但是岖免,如果在整個應(yīng)用中都使用相同的基類岳颇,繼承往往會導(dǎo)致一個脆弱的對象體系;而使用委托可能需要對委托對象進(jìn)行復(fù)雜的調(diào)用颅湘。

切面提供了取代繼承和委托的另一種可選方案话侧,而且在很多場景下更清晰簡潔。在使用面向切面編程時闯参,仍然在一個地方定義通用功能掂摔,但是可以通過聲明的方式定義這個功能要以何種方式在何處應(yīng)用,不需要修改受影響的類赢赊。橫切關(guān)注點可以被模塊化為特殊的類乙漓,這些類被稱為切面(aspect)。

這樣做有兩個好處:

  1. 現(xiàn)在每個關(guān)注點都集中于一個地方释移,而不是分散到多處代碼中叭披;
  2. 服務(wù)模塊更簡潔,因為它們只包含主要關(guān)注點(或核心功能)的代碼,而次要關(guān)注點的代碼被轉(zhuǎn)移到切面中了涩蜘。
定義AOP術(shù)語

描述切面的常用術(shù)語有通知(advice)嚼贡、切點(pointcut)和連接點(join point)。下圖展示了這些概念是如何關(guān)聯(lián)在一起的:

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

在AOP術(shù)語中粤策,切面要完成的工作被稱為通知。

通知定義了切面是什么以及何時使用误窖。除了描述切面要完成的工作叮盘,通知還解決了何時執(zhí)行這個工作的問題。

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

  • 前置通知(Before):在目標(biāo)方法被調(diào)用之前調(diào)用通知功能霹俺;
  • 后置通知(After):在目標(biāo)方法完成之后調(diào)用通知柔吼,此時不會關(guān)心方法的輸出是什么;
  • 返回通知(After-returning):在目標(biāo)方法成功執(zhí)行之后調(diào)用通知丙唧;
  • 異常通知(After-throwing):在目標(biāo)方法拋出異常后調(diào)用通知愈魏;
  • 環(huán)繞通知(Around):通知包裹了被通知的方法,在被通知的方法調(diào)用之前和調(diào)用之后執(zhí)行自定義的行為想际。
連接點(Join point)

應(yīng)用可能有數(shù)以千計的時機應(yīng)用通知培漏。這些時機被稱為連接點。連接點是在應(yīng)用執(zhí)行過程中能夠插入切面的一個點胡本。切面代碼可以利用這些點插入到應(yīng)用的正常流程之中北苟,并添加新的行為。

切點(Poincut)

一個切面并不需要通知應(yīng)用的所有連接點打瘪。切點有助于縮小切面所通知的連接點的范圍。

如果說通知定義了切面的“什么”和“何時”的話傻昙,那么切點就定義了“何處”闺骚。切點的定義會匹配通知所要織入的一個或多個連接點。通常使用明確的類和方法名稱妆档,或是利用正則表達(dá)式定義所匹配的類和方法名稱來指定這些切點僻爽。有些AOP框架允許創(chuàng)建動態(tài)的切點,可以根據(jù)運行時的決策來決定是否應(yīng)用通知贾惦。

切面(Aspect)

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

引入(Introduction)

引入允許向現(xiàn)有的類添加新方法或?qū)傩耘鼍怠亩梢栽跓o需修改這些現(xiàn)有的類的情況下,讓它們具有新的行為和狀態(tài)习瑰。

織入(Weaving)

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

  • 編譯期:切面在目標(biāo)類編譯時被織入甜奄。這種方式需要特殊的編譯器柠横。AspectJ的織入編譯器就是以這種方式織入切面的窃款。
  • 類加載期:切面在目標(biāo)類加載到JVM時被織入。這種方式需要特殊的類加載器(ClassLoader)牍氛,它可以在目標(biāo)類被引入應(yīng)用之前增強該目標(biāo)類的字節(jié)碼晨继。AspectJ 5的加載時織入(load-time weaving,LTW)就支持以這種方式織入切面搬俊。
  • 運行期:切面在應(yīng)用運行的某個時刻被織入紊扬。一般情況下,在織入切面時悠抹,AOP容器會為目標(biāo)對象動態(tài)地創(chuàng)建一個代理對象珠月。Spring AOP就是以這種方式織入切面的。
Spring對AOP的支持

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

  • 基于代理的經(jīng)典Spring AOP楔敌;
  • 純POJO切面啤挎;
  • @AspectJ注解驅(qū)動的切面;
  • 注入式AspectJ切面(適用于Spring各版本)卵凑。

前三種都是Spring AOP實現(xiàn)的變體庆聘,Spring AOP構(gòu)建在動態(tài)代理基礎(chǔ)之上,因此勺卢,Spring對AOP的支持局限于方法攔截伙判。

Spring的經(jīng)典AOP編程模型曾經(jīng)的確很棒,但是現(xiàn)在Spring提供了更簡潔和干凈的面向切面編程方式黑忱。宴抚。引入了簡單的聲明式AOP和基于注解的AOP之后,Spring經(jīng)典的AOP看起來就顯得非常笨重和過于復(fù)雜甫煞,直接使用ProxyFactory Bean會讓人感覺厭煩菇曲。

借助Spring的aop命名空間,可以將純POJO轉(zhuǎn)換為切面抚吠。實際上這些POJO只是提供了滿足切點條件時所要調(diào)用的方法常潮。這種技術(shù)需要XML配置

Spring借鑒了AspectJ的切面楷力,以提供注解驅(qū)動的AOP喊式。本質(zhì)上依然是Spring基于代理的AOP,但是編程模型幾乎與編寫成熟的AspectJ注解切面完全一致萧朝。這種AOP風(fēng)格的好處在于能夠不使用XML來完成功能岔留。

如果AOP需求超過了簡單的方法調(diào)用,那么需要考慮使用AspectJ來實現(xiàn)切面检柬。注入式AspectJ切面能夠幫助你將值注入到AspectJ驅(qū)動的切面中贸诚。

開始學(xué)習(xí)Spring AOP技術(shù)之前,必須要了解Spring AOP框架的一些關(guān)鍵知識。

Spring通知是Java編寫的

Spring所創(chuàng)建的通知都是用標(biāo)準(zhǔn)的Java類編寫的酱固。這樣就可以使用與普通Java開發(fā)一樣的集成開發(fā)環(huán)境(IDE)來開發(fā)切面械念。定義通知所應(yīng)用的切點通常會使用注解或在Spring配置文件里采用XML來編寫。

AspectJ與之相反运悲。AspectJ最初是以Java語言擴(kuò)展的方式實現(xiàn)的龄减。

Spring在運行時通知對象

通過在代理類中包裹切面,Spring在運行期把切面織入到Spring管理的bean中班眯。如下圖所示:

Spring的切面由包裹了目標(biāo)對象的代理類實現(xiàn)希停。代理類處理方法的調(diào)用,執(zhí)行額外的切面邏輯署隘,并調(diào)用目標(biāo)方法

代理類封裝了目標(biāo)類宠能,并攔截被通知方法的調(diào)用,再把調(diào)用轉(zhuǎn)發(fā)給真正的目標(biāo)bean磁餐。當(dāng)代理攔截到方法調(diào)用時违崇,在調(diào)用目標(biāo)bean方法之前,會執(zhí)行切面邏輯诊霹。

直到應(yīng)用需要被代理的bean時羞延,Spring才創(chuàng)建代理對象。如果使用的是ApplicationContext的話脾还,在ApplicationContext從BeanFactory中加載所有bean的時候伴箩,Spring才會創(chuàng)建被代理的對象。

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

通過使用各種AOP方案可以支持多種連接點模型鄙漏。因為Spring基于動態(tài)代理嗤谚,所以Spring只支持方法連接點。Spring缺少對字段連接點的支持怔蚌,無法讓我們創(chuàng)建細(xì)粒度的通知巩步,例如攔截對象字段的修改。而且它不支持構(gòu)造器連接點媚创,我們就無法在bean創(chuàng)建時應(yīng)用通知。

方法攔截可以滿足絕大部分的需求彤恶。其他連接點攔截功能可以利用Aspect來補充Spring AOP的功能钞钙。

通過切點來選擇連接點

切點用于準(zhǔn)確定位應(yīng)該在什么地方應(yīng)用切面的通知。

在Spring AOP中声离,要使用AspectJ的切點表達(dá)式語言來定義切點芒炼。

是Spring僅支持AspectJ切點指示器(pointcut designator)的一個子集。因為Spring是基于代理的术徊,而某些切點表達(dá)式是與基于代理的AOP無關(guān)的本刽。

下表列出了Spring AOP所支持的AspectJ切點指示器:

Spring借助AspectJ的切點表達(dá)式語言來定義Spring切面

嘗試使用AspectJ其他指示器時,將會拋出IllegalArgument-Exception異常。

這些Spring支持的指示器子寓,只有execution指示器是實際執(zhí)行匹配的暗挑,而其他的指示器都是用來限制匹配的。

編寫切面

定義一個Performance接口:

package concert;

public interface Performance{
    public void perform();
}

Performance可以代表任何類型的現(xiàn)場表演斜友。假設(shè)想編寫Performance的perform()方法觸發(fā)的通知炸裆。下圖展現(xiàn)了一個切點表達(dá)式,這個表達(dá)式設(shè)置當(dāng)perform()方法執(zhí)行時觸發(fā)通知的調(diào)用鲜屏。

使用AspectJ切點表達(dá)式來選擇Performance的perform()方法

我們使用execution()指示器選擇Performance的perform()方法烹看。方法表達(dá)式以“*”號開始,表明不關(guān)心方法返回值的類型洛史。然后惯殊,指定了全限定類名和方法名。對于方法參數(shù)列表也殖,使用兩個點號(..)表明切點選擇任意的perform()方法土思,無論該方法的入?yún)⑹鞘裁础?/p>

假設(shè)需要配置的切點僅匹配concert包”显矗可以使用within()指示器來限制匹配:

使用within()指示器限制切點范圍

注意使用了“&&”操作符把execution()和within()指示器連接在一起形成與(and)關(guān)系浪漠。類似地,可以使用“||”操作符來標(biāo)識或(or)關(guān)系霎褐,使用“!”操作符來標(biāo)識非(not)操作址愿。

由于“&”在XML中有特殊含義,在Spring的XML配置里面描述切點時冻璃,可以使用and來代替“&&”响谓。同樣,or和not可以分別用來代替“||”和“!”省艳。

在切點中選擇bean

除了上表所示的AspectJ指示器娘纷,Spring還引入了一個新的bean()指示器,它允許我們在切點表達(dá)式中使用bean的ID來標(biāo)識bean跋炕。bean()使用Bean ID或bean名稱作為參數(shù)來限制切點只匹配特定的bean赖晶。

例如:

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

在執(zhí)行Performance的perform()方法時應(yīng)用通知,限定的bean的ID為woodstock辐烂。

還可以使用非操作除了特定ID以外的其他bean應(yīng)用通知:

excution(* concert.Performance.perform()) 
    and !bean('woodstock')

切面的通知會被編織到所有ID不為woodstock的bean中遏插。

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

使用注解來創(chuàng)建切面是AspectJ 5所引入的關(guān)鍵特性。AspectJ 5之前纠修,編寫AspectJ切面需要學(xué)習(xí)一種Java語言的擴(kuò)展胳嘲,AspectJ面向注解的模型可以非常簡便地通過少量注解把任意類轉(zhuǎn)變?yōu)榍忻妗?/p>

定義切面

從演出的角度來看,觀眾非常重要扣草,但是對演出本身的功能來講了牛,它并不是核心颜屠,這是一個單獨的關(guān)注點。因此鹰祸,將觀眾定義為一個切面甫窟,并將其應(yīng)用到演出上就是較為明智的做法。

定義一個Audience類福荸,它定義了我們所需的一個切面:

Audience切面定義

Audience類使用@AspectJ注解進(jìn)行了標(biāo)注蕴坪。該注解表明Audience是一個切面。Audience類中的方法都使用注解來定義切面的具體行為敬锐。

Audience有四個方法背传,定義了一個觀眾在觀看演出時可能會做的事情。這些方法都使用了通知注解來表明它們應(yīng)該在什么時候調(diào)用台夺。AspectJ提供了五個注解來定義通知径玖,如下表所示:

使用AspectJ注解來聲明通知方法

Audience使用到了前面五個注解中的三個。takeSeats()silenceCellPhones()方法都用到了@Before注解颤介,表明它們應(yīng)該在演出開始之前調(diào)用梳星。applause()方法使用了@AfterReturning注解,它會在演出成功返回后調(diào)用滚朵。demandRefund()方法上添加了@AfterThrowing注解冤灾,這表明它會在拋出異常以后執(zhí)行。

代碼中所有的這些注解都給定了一個切點表達(dá)式作為它的值辕近,這四個方法的切點表達(dá)式都是相同的韵吨。如果我們只定義這個切點一次,然后每次需要的時候引用它移宅,效果可能會更好归粉。

@Pointcut注解能夠在一個@AspectJ切面內(nèi)定義可重用的切點

通過@Pointcut注解聲明頻繁使用的切點表達(dá)式

在Audience中,performance()方法使用了@Pointcut注解漏峰。為@Pointcut注解設(shè)置的值是一個切點表達(dá)式糠悼。通過在performance()方法上添加@Pointcut注解,擴(kuò)展了切點表達(dá)式語言浅乔,這樣就可以在任何的切點表達(dá)式中使用performance()倔喂。

performance()方法的實際內(nèi)容并不重要,該方法本身只是一個標(biāo)識靖苇,供@Pointcut注解依附席噩。

需要注意的是,除了注解和沒有實際操作的performance()方法顾复,Audience類依然是一個POJO班挖。能夠像使用其他的Java類那樣調(diào)用它的方法鲁捏,它的方法也能夠獨立地進(jìn)行單元測試芯砸。

像其他的Java類一樣萧芙,它可以裝配為Spring中的bean:

@Bean

public Audience audience() {
    return new Audience();
}

但是目前Audience仍然只是Spring容器中的一個bean。即便使用了AspectJ注解假丧,也并不會被視為切面双揪,這些注解不會解,也不會創(chuàng)建將其轉(zhuǎn)換為切面的代理包帚。

Java Config啟用AspectJ注解的自動代理

暫時略過

XML啟用AspectJ自動代理

要使用XML來裝配bean的話渔期,那么需要使用Spring aop命名空間中的<aop:aspectj-autoproxy>元素:

在XML中,通過aop命名空間啟用AspectJ自動代理

AspectJ自動代理會為使用@Aspect注解的bean創(chuàng)建一個代理渴邦,這個代理會圍繞著所有該切面的切點所匹配的bean疯趟。

,Spring的AspectJ自動代理僅僅使用@Aspect作為創(chuàng)建切面的指導(dǎo)谋梭,切面依然是基于代理的信峻。本質(zhì)上依然是Spring基于代理的切面。

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

環(huán)繞通知是最為強大的通知類型瓮床。它能夠讓你所編寫的邏輯將 被通知的目標(biāo)方法完全包裝起來盹舞。就像在一個通知方法中同時編寫前置通知和后置通知。

重新實現(xiàn)Audience切面:

環(huán)繞通知Audience

@Around注解表明watchPerformance()方法會作為performance()切點的環(huán)繞通知隘庄。在這個通知中踢步,觀眾在演出之前會將手機調(diào)至靜音并就坐,演出結(jié)束后會鼓掌喝彩丑掺。如果演出失敗的話获印,觀眾會要求退款。

新的通知方法接受ProceedingJoinPoint作為參數(shù)吼鱼,這個對象是必要的蓬豁,因為要在通知中通過它來調(diào)用被通知的方法。通知方法中當(dāng)要將控制權(quán)交給被通知的方法時菇肃,調(diào)用ProceedingJoinPoint的proceed()方法地粪。

如果不調(diào)proceed()方法,通知實際上會阻塞對被通知方法的調(diào)用琐谤。

處理通知中的參數(shù)

目前為止編寫的切面都很簡單蟆技,沒有任何參數(shù)。如果切面所通知的方法確實有參數(shù)該怎么辦呢斗忌?切面怎么訪問和使用傳遞給被通知方法的參數(shù)质礼?

重新看一下裝配Bean章節(jié)的BlankDisc樣例:

package soundsystem.properties;

import java.util.List;

import soundsystem.CompactDisc;

public class BlankDisc implements CompactDisc {

  private String title;
  private String artist;
  private List<String> tracks;

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

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

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

  public void play() {
    System.out.println("Playing " + title + " by " + artist);
    for (String track : tracks) {
      System.out.println("-Track: " + track);
    }
  }

}

play()方法會循環(huán)所有的磁道并調(diào)用playTrack()方法。但是织阳,我們也可以創(chuàng)建一個playTrack()方法直接播放某一個磁道中的歌曲眶蕉。

假設(shè)想記錄每個磁道被播放的次數(shù)。一種方法就是修改playTrack()方法唧躲,直接在每次調(diào)用的時候記錄這個數(shù)量造挽。但是碱璃,記錄磁道的播放次數(shù)與播放本身是不同的關(guān)注點,因此不應(yīng)該屬于playTrack()方法饭入。這應(yīng)該是切面要完成的任務(wù)嵌器。

為了記錄每個磁道所播放的次數(shù),創(chuàng)建了TrackCounter類谐丢,它是是通知playTrack()方法的一個切面:

使用參數(shù)化的通知來記錄磁道播放的次數(shù)

切面使用@Pointcut注解定義命名的切點爽航,并使用@Before將一個方法聲明為前置通知。這里的不同點在于切點還聲明了要提供給通知方法的參數(shù)乾忱。

在切點表達(dá)式中聲明參數(shù)讥珍,這個參數(shù)傳入到通知方法中

需要關(guān)注的是切點表達(dá)式中的args(trackNumber)限定符。它表明傳遞給playTrack()方法的int類型參數(shù)也會傳遞到通知中去窄瘟。參數(shù)的名稱trackNumber也與切點方法簽名中的參數(shù)相匹配串述。

這個參數(shù)會傳遞到通知方法中,這個通知方法通過@Before注解和命名切點trackPlayed(trackNumber)定義的寞肖。切點定義中的參數(shù)與切點方法中的參數(shù)名稱是一樣的纲酗,這樣就實現(xiàn)了從命名切點到通知方法的參數(shù)轉(zhuǎn)移。

現(xiàn)在新蟆,可以在Spring配置中將BlankDisc和TrackCounter定義為bean觅赊,并啟用AspectJ自動代理:

配置TrackCount記錄每個磁道播放的次數(shù)

編寫一個簡單測試。播放幾個磁道并通過TrackCounter斷言播放的數(shù)量琼稻。

測試TrackCounter切面

目前為止吮螺,所使用的切面中,所包裝的都是被通知對象的已有方法帕翻。方法包裝僅僅是切面所能實現(xiàn)的功能之一鸠补。接下來看如何通過編寫切面,為被通知的對象引入全新的功能嘀掸。

通過注解引入新功能

我們還沒有為對象增加任何新的方法紫岩,但是已經(jīng)為對象擁有的方法添加了新功能。如果切面能夠為現(xiàn)有的方法增加額外的功能睬塌,為什么不能為一個對象增加新的方法呢泉蝌?利用被稱為引入的AOP概念,切面可以為Spring bean添加新方法揩晴。

在Spring中勋陪,切面只是實現(xiàn)了它們所包裝bean相同接口的代理。如果除了實現(xiàn)這些接口硫兰,代理也能暴露新接口的話诅愚,切面所通知的bean看起來像是實現(xiàn)了新的接口,即便底層實現(xiàn)類并沒有實現(xiàn)這些接口也無所謂劫映。

使用Spring AOP违孝,可以為bean引入新的方法壕曼。代理攔截調(diào)用并委托給實現(xiàn)該方法的其他對象

當(dāng)引入接口的方法被調(diào)用時,代理會把此調(diào)用委托給實現(xiàn)了新接口的某個其他對象等浊。實際上,一個bean的實現(xiàn)被拆分到了多個類中摹蘑。

為了驗證這個思路筹燕,為示例中的所有的Performance實現(xiàn)引入下面的Encoreable接口:

package concert;

public interface Encoreable {
    void performEncore();
}

需要有一種方式將這個接口應(yīng)用到Performance實現(xiàn)中。現(xiàn)在假設(shè)能夠訪問Performance的所有實現(xiàn)衅鹿,并對其進(jìn)行修改撒踪,讓它們都實現(xiàn)Encoreable接口。從設(shè)計的角度來看大渤,這并不是最好的做法制妄,并不是所有的Performance都是具有Encoreable特性的。另外一方面泵三,有可能無法修改所有的Performance實現(xiàn)耕捞。

借助于AOP的引入功能,我們可以不必在設(shè)計上妥協(xié)或者侵入性地改變現(xiàn)有的實現(xiàn)烫幕。為了實現(xiàn)該功能俺抽,創(chuàng)建一個新的切面:

EncoreableIntroducer是一個切面。它與之前所創(chuàng)建的切面不同较曼,它并沒有提供前置磷斧、后置或環(huán)繞通知,而是通過@DeclareParents注解捷犹,將Encoreable接口引入到Performance bean中弛饭。

@DeclareParents注解由三部分組成:

  • value屬性指定了哪種類型的bean要引入該接口。在本例中萍歉,是所有實現(xiàn)Performance的類型侣颂。(標(biāo)記符后面的加號表示是Performance的所有子類型,而不是Performance本身枪孩。)
  • defaultImpl屬性指定了為引入功能提供實現(xiàn)的類横蜒。在這里指定的是DefaultEncoreable提供實現(xiàn)。
  • @DeclareParents注解所標(biāo)注的靜態(tài)屬性指明了要引入了接口销凑。在這里丛晌,我們所引入的是Encoreable接口。

和其他的切面一樣斗幼,我們需要在Spring應(yīng)用中將EncoreableIntroducer聲明為一個bean:

<bean class="concert.EncoreableIntroducer" />

Spring的自動代理機制將會獲取到它的聲明澎蛛,當(dāng)Spring發(fā)現(xiàn)一個bean使用了@Aspect注解時,Spring就會創(chuàng)建一個代理蜕窿,然后將調(diào)用委托給被代理的bean或被引入的實現(xiàn)谋逻,這取決于調(diào)用的方法屬于被代理的bean還是屬于被引入的接口呆馁。

Spring注解和自動代理提供了一種很便利的方式來創(chuàng)建切面。但是面向注解的切面聲明有一個明顯的劣勢:必須能夠為通知類添加注解毁兆。為了做到這一點浙滤,必須要有源碼。

如果沒有源碼的話气堕,或者不想將AspectJ注解放到代碼之中纺腊,Spring為切面提供了另外一種可選方案。

在XML中聲明切面

如果需要聲明切面茎芭,但是又不能為通知類添加注解的時候揖膜,就必須轉(zhuǎn)向XML配置。

在Spring的aop命名空間中梅桩,提供了多個元素用來在XML中聲明切面壹粟。

image.png

aop命名空間的其他元素能夠直接在Spring配置中聲明切面,而不需要使用注解宿百。

現(xiàn)在將Audience類的所有AspectJ注解全部移除掉:

盡管看起來并沒有什么差別趁仙,但Audience已經(jīng)具備了成為AOP通知的所有條件。

聲明前置和后置通知

使用Spring aop命名空間中的一些元素垦页,將沒有注解的Audience類轉(zhuǎn)換為切面:

通過XML將無注解的Audience聲明為切面

在<aop:config>元素內(nèi)幸撕,可以聲明一個或多個通知器、切面或者切點外臂。在上面的例子中坐儿,使用<aop:aspect>元素聲明了一個簡單的切面。ref元素引用了一個POJO bean宋光,該bean實現(xiàn)了切面的功能貌矿。ref元素所引用的bean提供了在切面中通知所調(diào)用的方法。

第一個需要注意的事項是大多數(shù)的AOP配置元素必須在<aop:config>元素的上下文內(nèi)使用罪佳。這條規(guī)則有幾種例外場景逛漫,但是把bean聲明為一個切面時,總是從<aop:config>元素開始配置的赘艳。

示例切面應(yīng)用了四個不同的通知酌毡。兩個<aop:before>元素定義了匹配切點的方法執(zhí)行之前調(diào)用前置通知方法(由method屬性聲明)。<aop:after-returning>元素定義了一個返回(after-returning)通知蕾管,在切點所匹配的方法調(diào)用之后再調(diào)用后置通知方法枷踏。<aop:after-throwing>元素定義了異常(after-throwing)通知掰曾,如果所匹配的方法執(zhí)行時拋出任何的異常,都將調(diào)用demandRefund()方法。下圖展示了通知邏輯如何織入到業(yè)務(wù)邏輯中:

Audience切面包含四種通知佑稠,它們把通知邏輯織入進(jìn)匹配切面切點的方法中

在所有的通知元素中旗芬,pointcut屬性定義了通知所應(yīng)用的切點,它的值是使用AspectJ切點表達(dá)式語法所定義的切點。

在基于AspectJ注解的通知中,當(dāng)發(fā)現(xiàn)這種類型的重復(fù)時,我們使用@Pointcut注解消除了這些重復(fù)的內(nèi)容娩井。在基于XML的切面聲明中,需要使用<aop:pointcut>元素洞辣。

如下的XML展現(xiàn)了如何將通用的切點表達(dá)式抽取到一個切點聲明中咐刨,,這樣這個聲明就能在所有的通知元素中使用了扬霜。

使用<aop:pointcut>定義命名切點

現(xiàn)在切點在一個地方定義的定鸟,并被多個通知元素引用。<aop:pointcut>元素定義了一個id為performance的切點著瓶。同時修改所有的通知元素联予,用 pointcut-ref 屬性來引用這個命名切點。

聲明環(huán)繞通知

前置通知和后置通知有一些限制材原。如果不使用成員變量存儲信息沸久,在前置通知和后置通知之間共享信息會非常麻煩。

希望并報告每個節(jié)目表演了多長時間余蟹。使用前置通知和后置通知實現(xiàn)該功能的唯一方式是在前置通知中記錄開始時間并在某個后置通知中報告表演耗費的時間卷胯。這樣的話我們必須在一個成員變量中保存開始時間。因為Audience是單例的威酒,如果像這樣保存狀態(tài)的話窑睁,將會存在線程安全問題。

環(huán)繞通知相比于前置通知和后置通知在這點上有明顯的優(yōu)勢葵孤,使用環(huán)繞通知担钮,我們可以完成前置通知和后置通知所實現(xiàn)的相同功能,且只需要在一個方法中實現(xiàn)尤仍。由于整個通知邏輯是在一個方法內(nèi)實現(xiàn)的裳朋,所以不需要使用成員變量保存狀態(tài)。

修改watchPerformance類:

watchPerformance()方法提供了AOP環(huán)繞通知

在切面中,watchPerformance()方法包含了之前四個通知方法的所有功能鲤嫡。且所有的功能都放在了這一個方法中送挑,因此這個方法還要負(fù)責(zé)自身的異常處理。

聲明環(huán)繞通知所需要做的僅僅是使用<aop:around>元素暖眼。

在XML中使用<aop:around>元素聲明環(huán)繞通知
使用XML為通知傳遞參數(shù)

使用XML來配置切面司澎,看一下如何完成這個任務(wù)栋豫。

首先丧鸯,要移除掉TrackCounter上所有的@AspectJ注解丛肢。

無注解的TrackCounter

去掉@AspectJ注解后蜂怎,除非顯式調(diào)用countTrack()方法杠步,否則TrackCounter不會記錄磁道播放的數(shù)量幽歼。借助一點Spring XML配置试躏,能夠讓TrackCounter重新變?yōu)榍忻妗?/p>

如下的程序展現(xiàn)了完整的Spring配置颠蕴,在這個配置中聲明了TrackCounter bean和BlankDisc bean犀被,并將TrackCounter轉(zhuǎn)化為切面:

在XML中將TrackCounter配置為參數(shù)化的切面

切點表達(dá)式中包含了一個參數(shù)掀泳,這個參數(shù)會傳遞到通知方法中员舵。(不使用“&&”是因為在XML中马僻,“&”符號會被解析為實體的開始)。

通過切面引入新的功能

AOP引入并不是AspectJ特有的措近。使用Spring aop命名空間中的<aop:declare-parents>元素瞭郑,可以實現(xiàn)相同的功能屈张。

下面的XML代碼片段與之前基于AspectJ的引入功能相同:

<aop:declare-parents>聲明了此切面所通知的bean要在它的對象層次結(jié)構(gòu)中擁有新的父類型菜拓。類型匹配Performance接口(由types-matching屬性指定)的那些bean在父類結(jié)構(gòu)中會增加Encoreable接口(由implementinterface屬性指定)。最后要解決的問題是Encoreable接口中的方法實現(xiàn)要來自于何處俺夕。

有兩種方式標(biāo)識所引入接口的實現(xiàn)劝贸。本例中映九,使用default-impl屬性用全限定類名顯式指定Encoreable的實現(xiàn)瞎颗。還可以使用delegate-ref屬性來標(biāo)識哼拔。

delegate-ref屬性引用了一個Spring bean作為引入的委托譬正。需要在Spring上下文中存在一個ID為encoreableDelegate的bean曾我。

<bean id="encoreableDelegate"
    class="concert.DefaultEncoreable" />

使用default-impl來直接標(biāo)識委托和間接使用delegate-ref的區(qū)別在于后者是Spring bean,它本身可以被注入斋荞、通知或使用其他的Spring配置平酿。

注入AspectJ切面

暫時跳過

小結(jié)

AOP是面向?qū)ο缶幊痰囊粋€強大補充蜈彼。通過AspectJ幸逆,現(xiàn)在可以把之前分散在應(yīng)用各處的行為放入可重用的模塊中还绘。這有效減少了代碼冗余栖袋,并讓我們的類關(guān)注自
身的主要功能塘幅。

Spring提供了一個AOP框架电媳,把切面插入到方法執(zhí)行的周圍匾乓。

關(guān)于在Spring應(yīng)用中如何使用切面,可以有多種選擇娱局。

當(dāng)Spring AOP不能滿足需求時铃辖,我們必須轉(zhuǎn)向更為強大的AspectJ娇斩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锦积,一起剝皮案震驚了整個濱河市丰介,隨后出現(xiàn)的幾起案子哮幢,更是在濱河造成了極大的恐慌橙垢,老刑警劉巖柜某,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喂击,死亡現(xiàn)場離奇詭異翰绊,居然都是意外死亡辞做,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門童叠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厦坛,“玉大人杜秸,你說我怎么就攤上這事撬碟。” “怎么了惶傻?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵银室,是天一觀的道長蜈敢。 經(jīng)常有香客問我扶认,道長辐宾,這世上最難降的妖魔是什么叠纹? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任誉察,我火速辦了婚禮持偏,結(jié)果婚禮上鸿秆,老公的妹妹穿的比我還像新娘卿叽。我一直安慰自己考婴,他們只是感情好沥阱,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布考杉。 她就那樣靜靜地躺著蛮寂,像睡著了一般易茬。 火紅的嫁衣襯著肌膚如雪抽莱。 梳的紋絲不亂的頭發(fā)上食铐,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天象泵,我揣著相機與錄音偶惠,去河邊找鬼朗涩。 笑死谢床,一個胖子當(dāng)著我的面吹牛识腿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蹋盆,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼伟众!你這毒婦竟也來了凳厢?” 一聲冷哼從身側(cè)響起先紫,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤居夹,失蹤者是張志新(化名)和其女友劉穎准脂,沒想到半個月后狸膏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體湾戳,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡院塞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了汹族。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片顶瞒。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖匀归,靈堂內(nèi)的尸體忽然破棺而出穆端,到底是詐尸還是另有隱情体啰,我是刑警寧澤嗽仪,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站兢孝,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏卷玉。R本人自食惡果不足惜相种,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望衬潦。 院中可真熱鬧,春花似錦漂羊、人聲如沸卸留。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽耻瑟。三九已至旨指,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間喳整,已是汗流浹背谆构。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留算柳,地道東北人低淡。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓猪杭,卻偏偏與公主長得像税手,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理疆瑰,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,651評論 18 139
  • 在生活中,監(jiān)控用電量是一個很重要的功能支示,但并不是大多數(shù)家庭重點關(guān)注的問題嘴纺。軟件系統(tǒng)的一些功能就像家里的電表一樣稳懒,這...
    yjaal閱讀 574評論 0 3
  • 幸福辙谜,不是長生不老,不是大魚大肉萍桌,不是權(quán)傾朝野藕施。幸福是每一個微小的生活愿望達(dá)成芙沥,如果人生沒有天長地久,那就珍惜曾經(jīng)...
    大樹姑娘閱讀 314評論 6 6
  • 以勝負(fù)來評定一場戰(zhàn)役享扔,是最直接于个,也是最模糊的。 按目的的達(dá)成與否,農(nóng)民無疑是勝利的。 因為在農(nóng)民的心里,這僅僅是農(nóng)...
    會跳舞的西紅柿閱讀 359評論 0 0
  • 前幾天,偶然重讀《七色花》這篇小學(xué)教科書的文章,大體上講了一個小女孩突然擁有了一朵七色花,一片花瓣能實現(xiàn)一個愿望,...
    一粒粒Eli閱讀 270評論 0 0