Spring之面向切面AOP

AOP概念

AOP的定義:AOP,Aspect Oriented Programming的縮寫筑悴,意為面向切面編程鸳劳,是通過預(yù)編譯或運行期動態(tài)代理實現(xiàn)程序功能處理的統(tǒng)一維護(hù)的一種技術(shù)。

系統(tǒng)中往往存在非常多的Controller匹配各種請求〖吒現(xiàn)在,如果我們想在每一個Controller的RequestMapping請求識別到的時候做個記錄格遭。正常的處理邏輯是在每個Request請求到的時候?qū)憘€Log哈街。然后執(zhí)行相應(yīng)的方法。但是這樣的話要把所有的RequestMapping方法都加上這個Log拒迅,顯然非常麻煩骚秦。面向切面就是希望只寫一遍Log就讓所有的請求都能夠執(zhí)行。橫向的將代碼嵌入到各個請求中璧微。如下圖那樣作箍,并不影響原本程序的原有邏輯。

image.png

接下來看一下幾個名詞

切面(Aspect):橫切關(guān)注點可以被模塊化為特殊的類被稱為切面(aspect)前硫。

通知(Advice):通知定義了切面是什么以及合適使用胞得。通常有五種類型:

  • 前置通知(Before):在目標(biāo)方法被調(diào)用前調(diào)用通知功能;
  • 后置通知(After):在目標(biāo)方法完成之后調(diào)用通知
  • 返回通知(After-returning):在目標(biāo)方法成功執(zhí)行后調(diào)用通知屹电;
  • 異常通知(After-throwing):在目標(biāo)方法拋出異常后調(diào)用通知阶剑;
  • 環(huán)繞通知(Around):通知包裹了被通知的方法,在通知的方法調(diào)用之前和調(diào)用之后執(zhí)行自定義的行為危号。

連接點(Join point):通知執(zhí)行的時機(jī)个扰。例如高速路上有非常多的出口,這些出口都相當(dāng)于連接點葱色。

切入點(Poincut):通知所要織入的具體位置递宅。還是以高速路為例子,眾多出口(連接點)中,我們只需要找到一個出口办龄,這個出口就是切入點烘绽。

引入(Introduction):允許我們向現(xiàn)有的類添加新方法和屬性。

織入(Weaving):把切面用到目標(biāo)對象并創(chuàng)建新的代理對象的過程俐填。

下圖為《Spring實戰(zhàn)》中關(guān)于各個名詞結(jié)合的圖安接。

image.png

Spring對AOP的支持

  • 基于代理的Spring AOP
  • 純POJO切面
  • @AspectJ注解驅(qū)動的切面
  • 注入式AspectJ切面(適用于Spring各版本)

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

通過切點選擇連接點

Spring支持Aspectj的指示器中只有execution指示器是實際執(zhí)行匹配的。
首先英融,我們有一個發(fā)送短信的接口

public interface MessageService {

    void sendMessage(String msg);

}

我們希望在MessageService進(jìn)行sendService方法時候觸發(fā)通知盏檐。就有了如下的表達(dá)式。

execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))
  • 表示可以返回任意類型驶悟,接下來是全限定類名胡野,方法。方法中的參數(shù)為 .. 表明可以使用任意參數(shù)痕鳍。

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

我們定義一個切面記錄方法的開始執(zhí)行的時間與執(zhí)行結(jié)束的時間硫豆,以及失敗的時間,此外還有@AfterRetrun和@Around

@Aspect
public class TimeLogging {

    @Before("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
    public void recordBeforeExecute() {
        System.out.println("start to send message at " + LocalDateTime.now());
    }

    @After("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
    public void recordAfterExecute() {
        System.out.println("message has sent seccess at " + LocalDateTime.now());
    }

    @AfterThrowing("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
    public void recordFailure() {
        System.out.println("fail to send message at " + LocalDateTime.now());
    }

}

顯然笼呆,上面同一個切入點表達(dá)式寫了三遍非常難看熊响。因此,可以使用@Pointcut注解定義命名的切點

@Aspect
public class TimeLogging {

    // 定義命名的切點
    @Pointcut("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
    public void sendMessage() {}

    @Before("sendMessage()")
    public void recordBeforeExecute() {
        System.out.println("start to send message at " + LocalDateTime.now());
    }

    @After("sendMessage()")
    public void recordAfterExecute() {
        System.out.println("message has sent seccess at " + LocalDateTime.now());
    }

    @AfterThrowing("sendMessage()")
    public void recordFailure() {
        System.out.println("fail to send message at " + LocalDateTime.now());
    }

}

接下來需要配置Bean及啟用AspectJ注解的自動代理诗赌,JavaConfig可以使用@EnableAspectJ-AutoProxy注解啟用汗茄;Xml可以使用Spring aop命名空間中的<aop:aspectj-autoproxy>元素。XML配置如下

<?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: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.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="messageService" class="com.dqzhou.spring.service.impl.MessageServiceImpl"/>
    <bean id="timeLogging" class="com.dqzhou.spring.aspectj.TimeLogging"/>

    <aop:aspectj-autoproxy/>
</beans>

最后铭若,測試一下調(diào)用messageService的sendMessage方法

public class SpringApplicationContext {
    
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        MessageService messageService = (MessageService) applicationContext.getBean("messageService");
        messageService.sendMessage("hello world!");
    }

}

結(jié)果如下剔难,Spring通過代理的方式增強(qiáng)方法

start to send message at 2019-10-28T00:16:11.746
hello world!
message has sent seccess at 2019-10-28T00:16:11.748
創(chuàng)建環(huán)繞通知

環(huán)繞通知相當(dāng)于在通知方法中同時編寫前置和后置通知。
更改切面類如下奥喻,ProceedingJoinPoint作為參數(shù)偶宫。通過它能夠調(diào)用被通知的方法。顯然环鲤,環(huán)繞通知可以輕易實現(xiàn)其它幾個注解所實現(xiàn)的功能纯趋,但是如果proceed()方法沒有調(diào)用,被通知的方法將無法訪問冷离。不過可以通過多次調(diào)用達(dá)到重試場景吵冒。

@Aspect
public class TimeLogging {

    // 定義命名的切點
    @Pointcut("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
    public void sendMessage() {}

    @Around("sendMessage()")
    public void watchMessage(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("start to send message at " + LocalDateTime.now());
            joinPoint.proceed();
            System.out.println("message has sent seccess at " + LocalDateTime.now());
        } catch (Throwable throwable) {
            System.out.println("fail to send message at " + LocalDateTime.now());
        }
    }

}
通過切面引入新功能

之前,Spring AOP通過代理已經(jīng)能夠?qū)Ρ煌ㄖ姆椒ㄟM(jìn)行增強(qiáng)西剥。那么痹栖,有沒有可能在不破壞,沒有嵌入原有類的結(jié)構(gòu)上增加新的方法瞭空。
@DeclareParents注解揪阿,做個標(biāo)記疗我,暫不展開

使用XML聲明切面

Spring的aop命名空間

  • <aop:advisor>:定義AOP通知器
  • <aop:after>:定義AOP后置通知(不管被通知的方法是否執(zhí)行成功)
  • <aop:after-returning>
  • <aop:after-throwing>
  • <aop:around>
  • <aop:aspect>:定義一個切面
  • <aop:aspectj-autoproxy>:啟用@AspectJ注解驅(qū)動的切面
  • <aop:before>
  • <aop:config>:頂層的AOP配置元素。大多數(shù)的<aop:*>元素必須包含
    在<aop:config>元素內(nèi)
  • <aop:declareparents>:以透明的方式為被通知的對象引入額外的接口
  • <aop:pointcut>:定義一個切點

創(chuàng)建一個切面類南捂,不加任何注解

public class LogAspect {
    
    public void logBeforeExecute() {
        System.out.println("method ready to execute at" + LocalDateTime.now());
    }

    public void logAfterExecute() {
        System.out.println("method has executed at" + LocalDateTime.now());
    }

    public void logFailure() {
        System.out.println("execute fail at " + LocalDateTime.now());
    }
    
    public void logAroundMessage(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("method ready to execute at" + LocalDateTime.now());
            joinPoint.proceed();
            System.out.println("method has executed at" + LocalDateTime.now());
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}

配置xml

    <bean id="logAspect" class="com.dqzhou.spring.aop.aspectj.LogAspect"/>
    <aop:config>
        <aop:aspect ref="logAspect">
            <aop:before pointcut="execution(* com.dqzhou.spring.aop.service.MessageService.sendMessage(..))" method="logBeforeExecute"/>
            <aop:after pointcut="execution(* com.dqzhou.spring.aop.service.MessageService.sendMessage(..))" method="logAfterExecute"/>
        </aop:aspect>
    </aop:config>

大多數(shù)aop配置必須在<aop:config>元素的上下文使用吴裤。然后<aop:config>可以聲明多個通知。同樣的溺健,寫多遍切點表達(dá)式很麻煩麦牺,可以使用<aop:pointcut>指定切點,然后通知通過pointcut-ref屬性來引用命名切點鞭缭,如下:

    <aop:config>
        <aop:aspect ref="logAspect">
            <aop:pointcut expression="execution(* com.dqzhou.spring.aop.service.MessageService.sendMessage(..))" id="logMessage"/>
            <aop:before pointcut-ref="logMessage" method="logBeforeExecute"/>
            <aop:after pointcut-ref="logMessage" method="logAfterExecute"/>
        </aop:aspect>
    </aop:config>

如果使用環(huán)繞通知剖膳,xml配置如下:

    <aop:config>
        <aop:aspect ref="logAspect">
            <aop:pointcut expression="execution(* com.dqzhou.spring.aop.service.MessageService.sendMessage(..))" id="logMessage"/>
            <aop:around pointcut-ref="logMessage" method="logAroundMessage"/>
        </aop:aspect>
    </aop:config>

總結(jié)

面向?qū)ο缶幊掏ㄟ^AOP把應(yīng)用各處的行為放入可重用的模塊中。減少了代碼的冗余岭辣。Spring提供了AOP的框架吱晒,可以通過XML或注解的方式快速進(jìn)行配置。但是如果SpringAOP不能滿足需求的時候易结,需要專項更為強(qiáng)大的AspectJ枕荞。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載柜候,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者搞动。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市渣刷,隨后出現(xiàn)的幾起案子鹦肿,更是在濱河造成了極大的恐慌,老刑警劉巖辅柴,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件箩溃,死亡現(xiàn)場離奇詭異,居然都是意外死亡碌嘀,警方通過查閱死者的電腦和手機(jī)涣旨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來股冗,“玉大人霹陡,你說我怎么就攤上這事≈棺矗” “怎么了烹棉?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長怯疤。 經(jīng)常有香客問我浆洗,道長,這世上最難降的妖魔是什么集峦? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任伏社,我火速辦了婚禮抠刺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘洛口。我一直安慰自己矫付,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布第焰。 她就那樣靜靜地躺著买优,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挺举。 梳的紋絲不亂的頭發(fā)上杀赢,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天,我揣著相機(jī)與錄音湘纵,去河邊找鬼脂崔。 笑死,一個胖子當(dāng)著我的面吹牛梧喷,可吹牛的內(nèi)容都是我干的砌左。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼铺敌,長吁一口氣:“原來是場噩夢啊……” “哼咳促!你這毒婦竟也來了泵肄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎难衰,沒想到半個月后互捌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赵颅,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡诱渤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了匾嘱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斤斧。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖霎烙,靈堂內(nèi)的尸體忽然破棺而出撬讽,到底是詐尸還是另有隱情,我是刑警寧澤吼过,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布锐秦,位于F島的核電站,受9級特大地震影響盗忱,放射性物質(zhì)發(fā)生泄漏酱床。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一趟佃、第九天 我趴在偏房一處隱蔽的房頂上張望扇谣。 院中可真熱鬧昧捷,春花似錦、人聲如沸罐寨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸯绿。三九已至跋破,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瓶蝴,已是汗流浹背毒返。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留舷手,地道東北人拧簸。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像男窟,于是被迫代替她去往敵國和親盆赤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348

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