摘要:AOP在spring中又叫“面向切面程式設計”,是對傳統我們物件導向程式設計的一個補充,主要操作物件就是“切面”,可以簡單的理解它是貫穿於方法之中,在方法執行前、執行時、執行後、返回值後、異常後要執行的操作。
本文分享自華為雲社群《一篇文搞懂《AOP面向切面程式設計》是一種什麼樣的體驗?》,作者: 灰小猿。
一、什麼是Spring的AOP?
AOP在spring中又叫“面向切面程式設計”,它可以說是對傳統我們物件導向程式設計的一個補充,從字面上顧名思義就可以知道,它的主要操作物件就是“切面”,所以我們就可以簡單的理解它是貫穿於方法之中,在方法執行前、執行時、執行後、返回值後、異常後要執行的操作。相當於是將我們原本一條線執行的程式在中間切開加入了一些其他操作一樣。
在應用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名稱空間的,
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種型別的通知註解:
- @Before:前置通知,在方法執行之前執行
- @After:後置通知,在方法執行之後執行
- @AfterRunning:返回通知,在方法返回結果之後執行
- @AfterThrowing:異常通知,在方法丟擲異常之後執行
- @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這個引數。
將切入點表示式應用到實際的切面類中如下:
@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介面來獲取到目標方法的資訊,如方法的返回值、方法名、引數型別等。
如我們在方法執行開始前,獲取到該目標方法的方法名和輸入的引數並輸出。
// 方法執行開始 @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就想到了一個辦法,重用切入點表示式。
也就是說將這些會重複使用的切入點表示式用一個方法來表示,那麼我們的通知註解只需要呼叫這個使用了該切入點表示式的方法即可實現和之前一樣的效果,這樣的話,我們即使想要更改切入點表示式的接入方法,也不用一個一個的去通知註解上修改了。
獲取可重用的切入點表示式的方法是:
- 隨便定義一個void的無實現的方法
- 為方法新增註解@Pointcut()
- 在註解中加入抽取出來的可重用的切入點表示式
- 使用value屬性將方法加入到對應的切面函式的註解中
完整例項如下:
@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切面程式設計的過程:
通過配置檔案實現切面
- 將目標類和切面類加入到容器中 相當於註解@component
- 宣告哪個類是切面類,相當於註解@Aspect
- 在配置檔案中配置五個通知方法,告訴切面類中的方法都何時執行
- 開啟基於註解的AOP功能
這裡有一點還需要注意:
當有兩個切面類和一個環繞方法時,方法的執行是按照配置檔案中配置的先後順序執行的,配置在前的就會先執行,配置在後的就會後執行,但同時環繞方法進入之後就會先執行環繞方法。
最後總結
至此通過AspectJ註解和XML配置兩種方式來實現AOP切面程式設計的過程就和大家分享完了,
總體來說基於註解的宣告要優先於基於XML的宣告。通過AspectJ註解,切面可以與AspectJ相容,而基於XML的配置則是Spring專有的。由於AspectJ得到越來越多的 AOP框架支援,所以以註解風格編寫的切面將會有更多重用的機會。