一篇文搞懂《AOP面向切面編程》是一種什么樣的體驗?(如何理解aop面向切面編程)
寫在前面
hello,大家好,我是灰小猿,一個超會寫bug的程序猿!
近期一直在準備秋招和技術,所以寫文的頻率有些下降,但是這也依然阻擋不了我要和大家一起分享技術的熱情呀。
之前在專欄《SSM編程日記》中和大家分享了很多關于SSM框架的相關知識和技術,其實創作該專欄的目的不僅僅是為了記錄自己學習過的技術,更是希望更多的小伙伴們能夠通過這個更進一步的進階Java這條不歸路!!!
所以今天我就繼續來和大家分享在spring框架中除IOC之外又一比較重要的技術點——AOP,接下來這篇文章,我將全面的和大家介紹AOP的概念、功能和詳細使用,只講實用性和重點!勵志一文帶你搞定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>,
我們在容器中需要添加這個元素,當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配置實現AOP切面編程的過程:
通過配置文件實現切面
將目標類和切面類加入到容器中 相當于注解@component
聲明哪個類是切面類,相當于注解@Aspect
在配置文件中配置五個通知方法,告訴切面類中的方法都何時運行
開啟基于注解的AOP功能
這里有一點還需要注意:
當有兩個切面類和一個環繞方法時,方法的執行是按照配置文件中配置的先后順序執行的,配置在前的就會先執行,配置在后的就會后執行,但同時環繞方法進入之后就會先執行環繞方法。
最后總結
至此通過AspectJ注解和XML配置兩種方式來實現AOP切面編程的過程就和大家分享完了,
總體來說基于注解的聲明要優先于基于XML的聲明。通過AspectJ注解,切面可以與AspectJ兼容,而基于XML的配置則是Spring專有的。由于AspectJ得到越來越多的 AOP框架支持,所以以注解風格編寫的切面將會有更多重用的機會。
最后通過這篇文章你有沒有對AOP切面編程有了一定的掌握呢?
歡迎小伙伴們評論區留言或者私信我交流,、以留備用喲!
我是灰小猿,我們下期見!
AOP Spring
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。