天天看點

一文帶你搞定AOP切面

摘要:AOP在spring中又叫“面向切面程式設計”,是對傳統我們面向對象程式設計的一個補充,主要操作對象就是“切面”,可以簡單的了解它是貫穿于方法之中,在方法執行前、執行時、執行後、傳回值後、異常後要執行的操作。

本文分享自華為雲社群《一篇文搞懂《AOP面向切面程式設計》是一種什麼樣的體驗?》,作者: 灰小猿。

一、什麼是Spring的AOP?

AOP在spring中又叫“面向切面程式設計”,它可以說是對傳統我們面向對象程式設計的一個補充,從字面上顧名思義就可以知道,它的主要操作對象就是“切面”,是以我們就可以簡單的了解它是貫穿于方法之中,在方法執行前、執行時、執行後、傳回值後、異常後要執行的操作。相當于是将我們原本一條線執行的程式在中間切開加入了一些其他操作一樣。

在應用AOP程式設計時,仍然需要定義公共功能,但可以明确的定義這個功能應用在哪裡,以什麼方式應用,并且不必修改受影響的類。這樣一來橫切關注點就被子產品化到特殊的類裡——這樣的類我們通常就稱之為“切面”。

例如下面這個圖就是一個AOP切面的模型圖,是在某一個方法執行前後執行的一些操作,并且這些操作不會影響程式本身的運作。

一文帶你搞定AOP切面

AOP切面程式設計中有一個比較專業的術語,我給大家羅切出來了:

一文帶你搞定AOP切面

現在大概的了解了AOP切面程式設計的基本概念,接下來就是實際操作了。

二、AOP架構環境搭建

1、導入jar包

目前比較流行且常用的AOP架構是AspectJ,我們在做SSM開發時用到的也是AspectJ,使用該架構技術就需要導入它所支援的jar包,

  • aopalliance.jar
  • aspectj.weaver.jar
  • spring-aspects.jar

關于SSM開發所使用的所有jar包和相關配置檔案我都已将幫大家準備好了!

點選連結下載下傳就能用。【全網最全】SSM開發必備依賴-Jar包、參考文檔、常用配置

2、引入AOP名稱空間

使用AOP切面程式設計時是需要在容器中引入AOP名稱空間的,

一文帶你搞定AOP切面

3、寫配置

其實在做AOP切面程式設計時,最常使用也必備的一個标簽就是,< aop:aspectj-autoproxy></aop:aspectj-autoproxy>,

我們在容器中需要添加這個元素,當Spring IOC容器偵測到bean配置檔案中的< aop:aspectj-autoproxy>元素時,會自動為與AspectJ切面比對的bean建立代理。

同時在現在的spring中使用AOP切面有兩種方式,分别是AspectJ注解或基于XML配置的AOP,

下面我依次和大家介紹一下這兩種方式的使用。

三、基于AspectJ注解的AOP開發

在上一篇文章中我也和大家将了關于spring中注解開發的強大,是以關于AOP開發我們同樣也可以使用注解的形式來進行編寫,下面我來和大家介紹一下如何使用注解方式書寫AOP。

1、五種通知注解

首先要在Spring中聲明AspectJ切面,隻需要在IOC容器中将切面聲明為bean執行個體。

當在Spring IOC容器中初始化AspectJ切面之後,Spring IOC容器就會為那些與 AspectJ切面相比對的bean建立代理。

在AspectJ注解中,切面隻是一個帶有@Aspect注解的Java類,它往往要包含很多通知。通知是标注有某種注解的簡單的Java方法。

AspectJ支援5種類型的通知注解:

  1. @Before:前置通知,在方法執行之前執行
  2. @After:後置通知,在方法執行之後執行
  3. @AfterRunning:傳回通知,在方法傳回結果之後執行
  4. @AfterThrowing:異常通知,在方法抛出異常之後執行
  5. @Around:環繞通知,圍繞着方法執行

2、切入點表達式規範

這五種通知注解後面還可以跟特定的參數,來指定哪一個切面方法在哪一個方法執行時觸發。那麼具體操作是怎麼樣的呢?

這裡就需要和大家介紹一個名詞:“切入點表達式”,通過在注解中加入該表達式參數,我們就可以通過表達式的方式定位一個或多個具體的連接配接點,

切入點表達式的文法格式規範是:

execution([權限修飾符] [傳回值類型] [簡單類名/全類名] [方法名] ([參數清單]))

其中在表達式中有兩個常用的特殊符号:

星号“ * ”代表所有的意思,星号還可以表示任意的數值類型

“.”号:“…”表示任意類型,或任意路徑下的檔案,

在這裡舉出幾個例子:

表達式:

execution(* com.atguigu.spring.ArithmeticCalculator.*(…))

含義:

ArithmeticCalculator接口中聲明的所有方法。第一個“”代表任意修飾符及任意傳回值。第二個“”代表任意方法。“…”比對任意數量、任意類型的參數。若目标類、接口與該切面類在同一個包中可以省略包名。

execution(public * ArithmeticCalculator.*(…))

ArithmeticCalculator接口的所有公有方法

execution(public double ArithmeticCalculator.*(…))

ArithmeticCalculator接口中傳回double類型數值的方法

execution(public double ArithmeticCalculator.*(double, …))

第一個參數為double類型的方法。“…” 比對任意數量、任意類型的參數。

execution(public double ArithmeticCalculator.*(double, double))

參數類型為double,double類型的方法

這裡還有一個定位最模糊的表達式:

execution("* *(…)")

表示任意包下任意類的任意方法,但是這個表達式千萬别寫,哈哈,不然你每一個執行的方法都會有通知方法執行的!

同時,在AspectJ中,切入點表達式可以通過 “&&”、“||”、“!”等操作符結合起來。

如:

execution (* .add(int,…)) || execution( *.sub(int,…))

表示任意類中第一個參數為int類型的add方法或sub方法

3、注解實踐

現在我們已經知道了注解和切入點表達式的使用,那麼接下來就是進行實踐了,

對于切入點表達式,我們可以直接在注解中使用“”寫在其中,還可以在@AfterReturning注解和@AfterThrowing注解中将切入點指派給pointcut屬性,但是在其他的注解中沒有pointcut這個參數。

一文帶你搞定AOP切面

将切入點表達式應用到實際的切面類中如下:

@Aspect    //切面注解
@Component    //其他業務層
public class LogUtli {
//    方法執行開始,表示目标方法是com.spring.inpl包下的任意類的任意以兩個int為參數,傳回int類型參數的方法
    @Before("execution(public int com.spring.inpl.*.*(int, int))")
    public static void LogStart(JoinPoint joinPoint) {
        System.out.println("通知記錄開始...");
    }
//    方法正常執行完之後
    /**
     * 在程式正常執行完之後如果有傳回值,我們可以對這個傳回值進行接收
     * returning用來接收方法的傳回值
     * */
    @AfterReturning(pointcut="public int com.spring.inpl.*.*(int, int)",returning="result")
    public static void LogReturn(JoinPoint joinPoint,Object result) {
        System.out.println("【" + joinPoint.getSignature().getName() + "】程式方法執行完畢了...結果是:" + result);
    }
}      

以上隻是一個最簡單的通知方法,但是在實際的使用過程中我們可能會将多個通知方法切入到同一個目标方法上去,比如同一個目标方法上既有前置通知、又有異常通知和後置通知。

但是這樣我們也隻是在目标方法執行時切入了一些通知方法,那麼我們能不能在通知方法中擷取到執行的目标方法的一些資訊呢?當然是可以的。

4、JoinPoint擷取方法資訊

在這裡我們就可以使用JoinPoint接口來擷取到目标方法的資訊,如方法的傳回值、方法名、參數類型等。

一文帶你搞定AOP切面

如我們在方法執行開始前,擷取到該目标方法的方法名和輸入的參數并輸出。

//    方法執行開始
    @Before("execution(public int com.spring.inpl.*.*(int, int))")
    public static void LogStart(JoinPoint joinPoint) {
            Object[] args = joinPoint.getArgs();    //擷取到參數資訊
            Signature signature = joinPoint.getSignature(); //擷取到方法簽名
            String name = signature.getName();    //擷取到方法名
            System.out.println("【" + name + "】記錄開始...執行參數:" + Arrays.asList(args));
    }      

5、接收方法的傳回值和異常資訊

對于有些目标方法在執行完之後可能會有傳回值,或者方法中途異常抛出,那麼對于這些情況,我們應該如何擷取到這些資訊呢?

首先我們來擷取當方法執行完之後擷取傳回值,

在這裡我們可以使用@AfterReturning注解,該注解表示的通知方法是在目标方法正常執行完之後執行的。

在傳回通知中,隻要将returning屬性添加到@AfterReturning注解中,就可以通路連接配接點的傳回值。

該屬性的值即為用來傳入傳回值的參數名稱,但是注意必須在通知方法的簽名中添加一個同名參數。

在運作時Spring AOP會通過這個參數傳遞傳回值,由于我們可能不知道傳回值的類型,是以一般将傳回值的類型設定為Object型。

與此同時,原始的切點表達式需要出現在pointcut屬性中,如下所示:

//    方法正常執行完之後
    /**
     * 在程式正常執行完之後如果有傳回值,我們可以對這個傳回值進行接收
     * returning用來接收方法的傳回值
     * */
    @AfterReturning(pointcut="public int com.spring.inpl.*.*(int, int)",returning="result")
    public static void LogReturn(JoinPoint joinPoint,Object result) {
            System.out.println("【" + joinPoint.getSignature().getName() + "】程式方法執行完畢了...結果是:" + result);
    }      

對于接收異常資訊,方法其實是一樣的。

我們需要将throwing屬性添加到@AfterThrowing注解中,也可以通路連接配接點抛出的異常。Throwable是所有錯誤和異常類的頂級父類,是以在異常通知方法可以捕獲到任何錯誤和異常。

如果隻對某種特殊的異常類型感興趣,可以将參數聲明為其他異常的參數類型。然後通知就隻在抛出這個類型及其子類的異常時才被執行。

執行個體如下:

//    異常抛出時
    /**
     * 在執行方法想要抛出異常的時候,可以使用throwing在注解中進行接收,
     * 其中value指明執行的全方法名
     * throwing指明傳回的錯誤資訊
     * */
    @AfterThrowing(pointcut="public int com.spring.inpl.*.*(int, int)",throwing="e")
    public static void LogThowing(JoinPoint joinPoint,Object e) {
        System.out.println("【" + joinPoint.getSignature().getName() +"】發現異常資訊...,異常資訊是:" + e);
    }      

6、環繞通知

我們在上面介紹通知注解的時候,大家應該也看到了其實還有一個很重要的通知——環繞通知,

環繞通知是所有通知類型中功能最為強大的,能夠全面地控制連接配接點,甚至可以控制是否執行連接配接點。

對于環繞通知來說,連接配接點的參數類型必須是ProceedingJoinPoint。它是 JoinPoint的子接口,允許控制何時執行,是否執行連接配接點。

在環繞通知中需要明确調用ProceedingJoinPoint的proceed()方法來執行被代理的方法。如果忘記這樣做就會導緻通知被執行了,但目标方法沒有被執行。這就意味着我們需要在方法中傳入參數ProceedingJoinPoint來接收方法的各種資訊。

注意:

環繞通知的方法需要傳回目标方法執行之後的結果,即調用 joinPoint.proceed();的傳回值,否則會出現空指針異常。

具體使用可以看下面這個執行個體:

/**
     * 環繞通知方法
     * 使用注解@Around()
     * 需要在方法中傳入參數proceedingJoinPoint 來接收方法的各種資訊
     * 使用環繞通知時需要使用proceed方法來執行方法
     * 同時需要将值進行傳回,環繞方法會将需要執行的方法進行放行
     * *********************************************
     * @throws Throwable 
     * */
    @Around("public int com.spring.inpl.*.*(int, int)")
    public Object MyAround(ProceedingJoinPoint pjp) throws Throwable {
 
//        擷取到目标方法内部的參數
        Object[] args = pjp.getArgs();
 
        System.out.println("【方法執行前】");
//        擷取到目标方法的簽名
        Signature signature = pjp.getSignature();
        String name = signature.getName();
        Object proceed = null;
        try {
//            進行方法的執行
            proceed = pjp.proceed();
            System.out.println("方法傳回時");
        } catch (Exception e) {
            System.out.println("方法異常時" + e);
        }finally{
            System.out.println("後置方法");
        }
 
        //将方法執行的傳回值傳回
        return proceed;
    }      

7、通知注解的執行順序

那麼現在這五種通知注解的使用方法都已經介紹完了,我們來總結一下這幾個通知注解都在同一個目标方法中時的一個執行順序。

在正常情況下執行:

@Before(前置通知)—>@After(後置通知)---->@AfterReturning(傳回通知)

在異常情況下執行:

@Before(前置通知)—>@After(後置通知)---->@AfterThrowing(異常通知)

當普通通知和環繞通知同時執行時:

執行順序是:

環繞前置----普通前置----環繞傳回/異常----環繞後置----普通後置----普通傳回/異常

8、重用切入點定義

對于上面的通知注解,我們都是在每一個通知注解上都定義了一遍切入點表達式,

但是試想一個問題,如果我們不想給這個方法設定通知方法了,或者我們想要将這些通知方法切入到另一個目标方法,那麼我們豈不是要一個一個的更改注解中的切入點表達式嗎?這樣也太麻煩了吧?

是以spring就想到了一個辦法,重用切入點表達式。

也就是說将這些會重複使用的切入點表達式用一個方法來表示,那麼我們的通知注解隻需要調用這個使用了該切入點表達式的方法即可實作和之前一樣的效果,這樣的話,我們即使想要更改切入點表達式的接入方法,也不用一個一個的去通知注解上修改了。

擷取可重用的切入點表達式的方法是:

  1. 随便定義一個void的無實作的方法
  2. 為方法添加注解@Pointcut()
  3. 在注解中加入抽取出來的可重用的切入點表達式
  4. 使用value屬性将方法加入到對應的切面函數的注解中
一文帶你搞定AOP切面

完整執行個體如下:

@Aspect    //切面注解
@Component    //其他業務層
public class LogUtli {
    /**
     * 定義切入點表達式的可重用方法
     * */
    @Pointcut("execution(public int com.spring.inpl.MyMathCalculator.*(int, int))")
    public void MyCanChongYong() {}
 
//    方法執行開始
    @Before("MyCanChongYong()")
    public static void LogStart(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();    //擷取到參數資訊
        Signature signature = joinPoint.getSignature(); //擷取到方法簽名
        String name = signature.getName();    //擷取到方法名
        System.out.println("【" + name + "】記錄開始...執行參數:" + Arrays.asList(args));
    }
//    方法正常執行完之後
    /**
     * 在程式正常執行完之後如果有傳回值,我們可以對這個傳回值進行接收
     * returning用來接收方法的傳回值
     * */
    @AfterReturning(value="MyCanChongYong()",returning="result")
    public static void LogReturn(JoinPoint joinPoint,Object result) {
        System.out.println("【" + joinPoint.getSignature().getName() + "】程式方法執行完畢了...結果是:" + result);
    }
 
//    異常抛出時
    /**
     * 在執行方法想要抛出異常的時候,可以使用throwing在注解中進行接收,
     * 其中value指明執行的全方法名
     * throwing指明傳回的錯誤資訊
     * */
    @AfterThrowing(value="MyCanChongYong()",throwing="e")
    public static void LogThowing(JoinPoint joinPoint,Object e) {
        System.out.println("【" + joinPoint.getSignature().getName() +"】發現異常資訊...,異常資訊是:" + e);
    }
 
//    結束得出結果
    @After(value = "execution(public int com.spring.inpl.MyMathCalculator.add(int, int))")
    public static void LogEnd(JoinPoint joinPoint) {
        System.out.println("【" + joinPoint.getSignature().getName() +"】執行結束");
    }
 
    /**
     * 環繞通知方法
     * @throws Throwable 
     * */
    @Around("MyCanChongYong()")
    public Object MyAround(ProceedingJoinPoint pjp) throws Throwable {
 
//        擷取到目标方法内部的參數
        Object[] args = pjp.getArgs();
 
        System.out.println("【方法執行前】");
//        擷取到目标方法的簽名
        Signature signature = pjp.getSignature();
        String name = signature.getName();
        Object proceed = null;
        try {
//            進行方法的執行
            proceed = pjp.proceed();
            System.out.println("方法傳回時");
        } catch (Exception e) {
            System.out.println("方法異常時" + e);
        }finally{
            System.out.println("後置方法");
        }
 
        //将方法執行的傳回值傳回
        return proceed;
    }
}      

以上就是使用AspectJ注解實作AOP切面的全部過程了,

在這裡還有一點特别有意思的規定提醒大家,就是當你有多個切面類時,切面類的執行順序是按照類名的首字元先後來執行的(不區分大小寫)。

接下來我來和大家講解一下實作AOP切面程式設計的另一種方法——基于XML配置的AOP實作。

四、基于XML配置的AOP實作

基于XML配置的AOP切面顧名思義就是摒棄了注解的使用,轉而在IOC容器中配置切面類,這種聲明是基于aop名稱空間中的XML元素來完成的,

在bean配置檔案中,所有的Spring AOP配置都必須定義在< aop:config>元素内部。對于每個切面而言,都要建立一個< aop:aspect>元素來為具體的切面實作引用後端bean執行個體。

切面bean必須有一個辨別符,供< aop:aspect>元素引用。

是以我們在bean的配置檔案中首先應該先将所需切面類加入到IOC容器中去,之後在aop的元素标簽中進行配置。我們在使用注解進行開發的時候,五種通知注解以及切入點表達式這些在xml檔案中同樣是可以配置出來的。

1、聲明切入點

切入點使用

< aop:pointcut>元素聲明。

切入點必須定義在< aop:aspect>元素下,或者直接定義在< aop:config>元素下。

定義在< aop:aspect>元素下:隻對目前切面有效

定義在< aop:config>元素下:對所有切面都有效

基于XML的AOP配置不允許在切入點表達式中用名稱引用其他切入點。

2、聲明通知

在aop名稱空間中,每種通知類型都對應一個特定的XML元素。

通知元素需要使用< pointcut-ref>來引用切入點,或用< pointcut>直接嵌入切入點表達式。

method屬性指定切面類中通知方法的名稱

具體使用可以看下面這裡執行個體:

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

    <!-- 通過配置檔案實作切面 
        1、将目标類和切面類加入到容器中 @component
        2、聲明哪個類是切面類,@Aspect
        3、在配置檔案中配置五個通知方法,告訴切面類中的方法都何時運作
        4、開啟基于注解的AOP功能
    -->
 
    <!-- 将所需類加入到容器中 -->
    <bean id="myCalculator" class="com.spring.inpl.MyMathCalculator"></bean>
    <bean id="logUtil" class="com.spring.utils.LogUtli"></bean>
    <bean id="SecondUtli" class="com.spring.utils.SecondUtli"></bean>
 
    <!-- 進行基于AOP的配置 -->
    <!-- 當有兩個切面類和一個環繞方法時,方法的執行是按照配置檔案中配置的先後順序執行的
        配置在前的就會先執行,配置在後的就會後執行,但同時環繞方法進入之後就會先執行環繞方法
     -->
    <aop:config>
        <!-- 配置一個通用類 -->
        <aop:pointcut expression="execution(public int com.spring.inpl.MyMathCalculator.*(int, int)))" id="myPoint"/>
        <!-- 配置某一個指定的切面類 -->
        <aop:aspect id="logUtil_Aspect" ref="logUtil">
            <!-- 為具體的方法進行指定
            method指定具體的方法名
            pointcut指定具體要對應的方法
             -->
            <aop:before method="LogStart" pointcut="execution(public int com.spring.inpl.MyMathCalculator.add(int, int))"/>
            <aop:after-throwing method="LogThowing" pointcut="execution(public int com.spring.inpl.MyMathCalculator.*(int, int)))" throwing="e"/>
            <aop:after-returning method="LogReturn" pointcut-ref="myPoint" returning="result"/>
            <aop:after method="LogEnd" pointcut-ref="myPoint"/>
            <!-- 定義一個環繞方法 -->
            <aop:around method="MyAround" pointcut-ref="myPoint"/>
        </aop:aspect>
 
        <!-- 定義第二個切面類 -->
        <aop:aspect ref="SecondUtli">
            <aop:before method="LogStart" pointcut="execution(public int com.spring.inpl.MyMathCalculator.*(..))"/>
            <aop:after-throwing method="LogThowing" pointcut-ref="myPoint" throwing="e"/>
            <aop:after method="LogEnd" pointcut-ref="myPoint"/>
        </aop:aspect>
 
    </aop:config>
</beans>      

總結一下通過XML配置實作AOP切面程式設計的過程:

通過配置檔案實作切面

  1. 将目标類和切面類加入到容器中 相當于注解@component
  2. 聲明哪個類是切面類,相當于注解@Aspect
  3. 在配置檔案中配置五個通知方法,告訴切面類中的方法都何時運作
  4. 開啟基于注解的AOP功能

這裡有一點還需要注意:

當有兩個切面類和一個環繞方法時,方法的執行是按照配置檔案中配置的先後順序執行的,配置在前的就會先執行,配置在後的就會後執行,但同時環繞方法進入之後就會先執行環繞方法。

最後總結

至此通過AspectJ注解和XML配置兩種方式來實作AOP切面程式設計的過程就和大家分享完了,

總體來說基于注解的聲明要優先于基于XML的聲明。通過AspectJ注解,切面可以與AspectJ相容,而基于XML的配置則是Spring專有的。由于AspectJ得到越來越多的 AOP架構支援,是以以注解風格編寫的切面将會有更多重用的機會。

點選關注,第一時間了解華為雲新鮮技術~