Spring之面向切面AOP

AOP概念

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

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

image.png

接下來看一下幾個(gè)名詞

切面(Aspect):橫切關(guān)注點(diǎ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í)行自定義的行為。

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

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

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

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

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

image.png

Spring對AOP的支持

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

注:Spring只支持方法級別的連接點(diǎn)

通過切點(diǎn)選擇連接點(diǎn)

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

public interface MessageService {

    void sendMessage(String msg);

}

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

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

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

我們定義一個(gè)切面記錄方法的開始執(zhí)行的時(shí)間與執(zhí)行結(jié)束的時(shí)間,以及失敗的時(shí)間,此外還有@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());
    }

}

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

@Aspect
public class TimeLogging {

    // 定義命名的切點(diǎn)
    @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注解的自動(dòng)代理,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)于在通知方法中同時(shí)編寫前置和后置通知。
更改切面類如下,ProceedingJoinPoint作為參數(shù)。通過它能夠調(diào)用被通知的方法。顯然,環(huán)繞通知可以輕易實(shí)現(xiàn)其它幾個(gè)注解所實(shí)現(xiàn)的功能,但是如果proceed()方法沒有調(diào)用,被通知的方法將無法訪問。不過可以通過多次調(diào)用達(dá)到重試場景。

@Aspect
public class TimeLogging {

    // 定義命名的切點(diǎn)
    @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注解,做個(gè)標(biāo)記,暫不展開

使用XML聲明切面

Spring的aop命名空間

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

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

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>可以聲明多個(gè)通知。同樣的,寫多遍切點(diǎn)表達(dá)式很麻煩,可以使用<aop:pointcut>指定切點(diǎn),然后通知通過pointcut-ref屬性來引用命名切點(diǎn),如下:

    <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不能滿足需求的時(shí)候,需要專項(xiàng)更為強(qiáng)大的AspectJ。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容