湖南建設(shè)人力資源官方網(wǎng)站萬能軟文模板
??前面的話??
本篇文章將介紹一種特別重要的思想,AOP(Aspect Oriented Programming),即面向切面編程,可以說是OOP(Object Oriented Programming,面向?qū)ο缶幊?#xff09;的補(bǔ)充和完善。
AOP把軟件系統(tǒng)分為兩個部分:核心關(guān)注點(diǎn)和橫切關(guān)注點(diǎn)。業(yè)務(wù)處理的主要流程是核心關(guān)注點(diǎn),與之關(guān)系不大的部分是橫切關(guān)注點(diǎn)。
📒博客主頁:未見花聞的博客主頁
🎉歡迎關(guān)注🔎點(diǎn)贊👍收藏??留言📝
📌本文由未見花聞原創(chuàng),CSDN首發(fā)!
📆首發(fā)時間:🌴2022年9月6日🌴
📆修改時間:🌴2023年4月11日🌴
??堅持和努力一定能換來詩與遠(yuǎn)方!
💭推薦書籍:📚《Spring實(shí)戰(zhàn)》,📚《SpringBoot實(shí)戰(zhàn)》
💬參考在線編程網(wǎng)站:🌐??途W(wǎng)🌐力扣
博主的碼云gitee,平常博主寫的程序代碼都在里面。
博主的github,平常博主寫的程序代碼都在里面。
🍭作者水平很有限,如果發(fā)現(xiàn)錯誤,一定要及時告知作者哦!感謝感謝!
📌導(dǎo)航小助手📌
- 1.面向切面編程AOP
- 1.1什么是AOP?
- 1.2AOP的作用
- 1.3AOP的核心概念
- 2.Spring AOP
- 2.1Spring AOP的使用
- 2.2AspectJ表達(dá)式基本語法
- 2.3拋出異常后通知與環(huán)繞通知
- 2.4Spring AOP的實(shí)現(xiàn)原理
1.面向切面編程AOP
1.1什么是AOP?
AOP(Aspect Oriented Programming),即面向切面編程,可以說是OOP(Object Oriented Programming,面向?qū)ο缶幊?#xff09;的補(bǔ)充和完善。OOP引入封裝、繼承、多態(tài)等概念來建立一種對象層次結(jié)構(gòu),用于模擬公共行為的一個集合。不過OOP允許開發(fā)者定義縱向的關(guān)系,但并不適合定義橫向的關(guān)系,例如日志功能。日志代碼往往橫向地散布在所有對象層次中,而與它對應(yīng)的對象的核心功能毫無關(guān)系對于其他類型的代碼,如安全性、異常處理和透明的持續(xù)性也都是如此,這種散布在各處的無關(guān)的代碼被稱為橫切(cross cutting),在OOP設(shè)計中,它導(dǎo)致了大量代碼的重復(fù),而不利于各個模塊的重用。
AOP技術(shù)恰恰相反,它利用一種稱為"橫切"的技術(shù),剖解開封裝的對象內(nèi)部,并將那些影響了多個類的公共行為封裝到一個可重用模塊,并將其命名為"Aspect",即切面。所謂"切面",簡單說就是那些與業(yè)務(wù)無關(guān),卻為業(yè)務(wù)模塊所共同調(diào)用的邏輯或責(zé)任封裝起來,便于減少系統(tǒng)的重復(fù)代碼,降低模塊之間的耦合度,并有利于未來的可操作性和可維護(hù)性。
使用"橫切"技術(shù),AOP把軟件系統(tǒng)分為兩個部分:核心關(guān)注點(diǎn)和橫切關(guān)注點(diǎn)。業(yè)務(wù)處理的主要流程是核心關(guān)注點(diǎn),與之關(guān)系不大的部分是橫切關(guān)注點(diǎn)。橫切關(guān)注點(diǎn)的一個特點(diǎn)是,他們經(jīng)常發(fā)生在核心關(guān)注點(diǎn)的多處,而各處基本相似,比如權(quán)限認(rèn)證、日志、事物。AOP的作用在于分離系統(tǒng)中的各種關(guān)注點(diǎn),將核心關(guān)注點(diǎn)和橫切關(guān)注點(diǎn)分離開來。
1.2AOP的作用
想象一個場景,我們在做后臺系統(tǒng)時,除了登錄和注冊等幾個功能不需要做用戶登錄驗(yàn)證之外,其他幾乎所有頁面都需要先驗(yàn)證用戶登錄的狀態(tài),那這個時候我們要怎么處理呢?
如果不使用AOP,我們就需要在每一個Controller層都寫一遍驗(yàn)證用戶是否已經(jīng)登錄的程序,如果你實(shí)現(xiàn)的功能有很多,并且這些功能都需要進(jìn)行登錄驗(yàn)證,那你就需要編寫大量重復(fù)的代碼,非常的麻煩,盡管你可以將登錄驗(yàn)證實(shí)現(xiàn)的邏輯封裝在一個方法中,但是你要在很多地方調(diào)用這個方法,還是很麻煩。
如果使用AOP,在進(jìn)入核心的業(yè)務(wù)代碼之前會做統(tǒng)一的一個攔截,去驗(yàn)證用戶是否登錄,這樣就很方便,僅需做一個攔截工作,再將驗(yàn)證代碼一執(zhí)行即可。
除了登錄驗(yàn)證功能之外,還有很多功能也可以使用AOP,比如:
- 統(tǒng)一日志記錄與持久化。
- 統(tǒng)一方法執(zhí)行時間統(tǒng)計。
- 統(tǒng)一數(shù)據(jù)返回格式。
- 統(tǒng)一處理程序中的異常。
- 統(tǒng)一事務(wù)的開啟與提交。
也就是說使用 AOP 可以擴(kuò)充多個對象的某個能力,所以 AOP 可以說是 OOP (Object Oriented Programming,面向?qū)ο缶幊?#xff09;的補(bǔ)充和完善。
1.3AOP的核心概念
1、橫切關(guān)注點(diǎn)
想要對哪些方法或類進(jìn)行攔截,攔截后怎么處理,這些關(guān)注點(diǎn)稱之為橫切關(guān)注點(diǎn)。
2、切面(aspect)
類是對物體特征的抽象,切面就是對橫切關(guān)注點(diǎn)的抽象,你可以認(rèn)為切面相當(dāng)于橫切關(guān)注點(diǎn)。
3、連接點(diǎn)(joinpoint)
被攔截到的點(diǎn),因?yàn)镾pring只支持方法類型的連接點(diǎn),所以在Spring中連接點(diǎn)指的就是被攔截到的方法,實(shí)際上連接點(diǎn)還可以是字段或者構(gòu)造器。
4、切入點(diǎn)(pointcut)
提供一組規(guī)則,根據(jù)規(guī)則匹配合法的連接點(diǎn),滿足規(guī)則的連接點(diǎn)可以理解為切點(diǎn),然后可以為切點(diǎn)提供具體的處理(通知)。
5、通知(advice)
所謂通知指的就是指攔截到連接點(diǎn)之后要執(zhí)行的代碼,或者說在切點(diǎn)出所需要執(zhí)行的代碼是什么。
通知包含前置通知,后置通知,返回之后通知,拋異常后通知與環(huán)繞通知五類。
在Spring切面類中,可以在方法上使用以下注解,會設(shè)置方法為通知方法,在滿足條件后會調(diào)用對應(yīng)滿足條件的方法:
- 前置通知使用@Before∶通知方法會在目標(biāo)方法調(diào)用之前執(zhí)行。
- 后置通知使用@After∶通知方法會在目標(biāo)方法返回或者拋出異常后調(diào)用。
- 返回之后通知使用@AfterReturning∶ 通知方法會在目標(biāo)方法返回后調(diào)用。
- 拋異常后通知使用@AfterThrowing∶ 通知方法會在目標(biāo)方法拋出異常后調(diào)用。
- 環(huán)繞通知使用@Around∶通知包裹了被通知的方法,在被通知的方法通知之前和調(diào)用之后執(zhí)行自定義的行為。
6、目標(biāo)對象
代理的目標(biāo)對象。
7、織入(weaving)
織入(weaving)即代理的生成時機(jī),
織入是把切面應(yīng)用到目標(biāo)對象并創(chuàng)建新的代理對象的過程,切面在指定的連接點(diǎn)被織入到目標(biāo)對象中。
在目標(biāo)對象的生命周期里有多個點(diǎn)可以進(jìn)行織入∶
- 編譯期∶切面在目標(biāo)類編譯時被織入。這種方式需要特殊的編譯器。AspectJ的織入編譯器就是以這種方式織入切面的。
- 類加載器∶切面在目標(biāo)類加載到JVM時被織入。這種方式需要特殊的類加載器(ClassLoader),它可以在目標(biāo)類被引入應(yīng)用之前增強(qiáng)該目標(biāo)類的字節(jié)碼。AspectJ5的加載時織入(load-time weaving.LTW)就支持以這種方式織入切面。
- 運(yùn)行期∶切面在應(yīng)用運(yùn)行的某一時刻被織入。一般情況下,在織入切面時,AOP容器會為目標(biāo)對象動態(tài)創(chuàng)建一個代理對象。SpringAOP就是以這種方式織入切面的。
8、引入(introduction)
在不修改代碼的前提下,引入可以在運(yùn)行期為類動態(tài)地添加一些方法或字段。
2.Spring AOP
面向切面編程是一種思想,Spring AOP是AOP的一種實(shí)現(xiàn)。
2.1Spring AOP的使用
SpringAOP使用的主要步驟為:
第一步,在SpringBoot項目中添加AOP相關(guān)的依賴。
第二步,定義切面。
第三步,定義切點(diǎn)。
第四步,實(shí)現(xiàn)通知。
第一步,在SpringBoot項目中添加AOP相關(guān)的依賴,就是在Maven的配置文件中添加aop的依賴。
由于使用Edit Starters插件訪問官方的源是找不到有關(guān)SpringBoot的AOP依賴,這是因?yàn)樵趇dea中,上面只列舉了一些常用的依賴,不是所有依賴都在上面,如果找不到我們就去Maven中央倉庫中去尋找。
搜索一下,找到這個依賴,然后進(jìn)去復(fù)制依賴信息拷貝到Maven的配置文件中就行。
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
第二步,定義切面,在spring boot項目中其實(shí)就是加上@Aspect和@Component注解的一個類,這個類就表示一個切面。
//設(shè)置切面,這個類就是一個切面
@Aspect
@Component
public class UserAspect {...
}
第三步,在切面里面定義切點(diǎn),在Spring中其實(shí)本質(zhì)上就是一個方法,具體說是使用 @Pointcut注解修飾的一個方法,該方法不需要配置任何信息。
//定義切點(diǎn),設(shè)置攔截規(guī)則@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")public void pointcut() {}
其中@Pointcut注解中的參數(shù)是一個AspectJ表達(dá)式,它的作用就是設(shè)置哪些返回值類型哪些類的哪些方法需要攔截可以指定到參數(shù)列表。
第四步,實(shí)現(xiàn)通知,本質(zhì)上就是實(shí)現(xiàn)一個方法,只不過在方法上加上不同通知類型的注解即可,如前置通知加上@Before注解,注解的參數(shù)為切點(diǎn)方法名。
//前置通知@Before("pointcut()")public void doBefore() {System.out.println("執(zhí)行Before通知");}
同理,后置通知也是如此,就是將注解改為@After:
//后置通知@After("pointcut()")public void doAfter() {System.out.println("執(zhí)行After通知");}
以及目標(biāo)方法返回后通知@AfterReturning:
//返回之后通知@AfterReturning("pointcut()")public void doAfterRunning() {System.out.println("執(zhí)行AfterRunning通知");}
我們來驗(yàn)證一下上述設(shè)置切面攔截代碼的正確性,我們寫一個在攔截范圍的類以及方法:
啟動程序,我們訪問頁面http://127.0.0.1:8080/user/hello
,看看控制臺的輸出:
通過運(yùn)行結(jié)果我們也能夠看出上面三種通知方式執(zhí)行的時機(jī)以及先后順序。
2.2AspectJ表達(dá)式基本語法
*
∶匹配任意字符,只匹配一個元素(包,類,或方法,方法參數(shù))
..
∶匹配任意字符,可以匹配多個元素,在表示類時,必須和*
聯(lián)合使用,匹配參數(shù)列表時表示匹配所有類型的參數(shù)列表。
+
∶ 表示按照類型匹配指定類及其所有子類,必須跟在類名后面,如com.Car+,表示攔截Cat類以及繼承Cat類的所有子類。
切點(diǎn)表達(dá)式由切點(diǎn)函數(shù)組成,其中execution()
是最常用的切點(diǎn)函數(shù),用來匹配方法,語法為∶
execution(<權(quán)限修飾符><返回類型><包.類.方法(參數(shù))><異常>)
其中權(quán)限修飾符與異常項一般省略,返回類型方法以及參數(shù)不可省略,其他項可以省略。
權(quán)限修飾符:
- 填寫權(quán)限修飾符,就只會匹配相應(yīng)修飾符修飾的方法。
- 省略,權(quán)限不作為限制,所有權(quán)限修飾符的方法都會匹配。
返回類型,必須參數(shù),不可省略:
- 填寫具體返回類型,就匹配相應(yīng)返回類型的方法。
*
表示匹配所有返回值類型的方法。
包,類,一般情況下要有,但是可以省略:
- 填寫包和類,就只匹配你所規(guī)定的包或類。
*
表示匹配某目錄下所有的包或者類。+
作用在類上,匹配該類以及繼承該類的所有子類。
方法,表示需要匹配方法的名字,參數(shù)表示需要匹配參數(shù)列表的類型,不可省略:
- 指定方法名和參數(shù)列表,就只匹配你所限定的方法。
*
可以作用在方法匹配字段上,表示匹配某類中所有的方法。..
可以作用在參數(shù)列表上,對參數(shù)列表類型不做限制。
異常,可以匹配拋出指定異常的方法,該參數(shù)一般省略。
下面來看幾個例子,我們來了解一下AspectJ表達(dá)式:
execution(* com.cad.demo.User.*(..))
∶匹配User類里的所有方法。
execution(* com.cad.demo.User+.*(..))
∶匹配User類及其子類中的所有方法。
execution(* com.cad.*.*(..))
∶匹配com.cad包下的所有類的所有方法。
execution(* com.cad..*.*(..))
∶匹配 com.cad 包下、子孫包下所有類的所有方法。
execution(* addUser(String,int))
∶ 匹配 addUser 方法,且第一個參數(shù)類型是 String,第二個參數(shù)類型是int。
2.3拋出異常后通知與環(huán)繞通知
前面我們已經(jīng)介紹了前置通知,后者通知以及返回后通知的演示,下面我們繼續(xù)介紹剩下兩種通知,在上面已經(jīng)實(shí)現(xiàn)代碼基礎(chǔ)上,我們繼續(xù)添加通知來進(jìn)行演示。
拋出異常后通知,其實(shí)和前面三種通知的用法可以說一模一樣,只不過只有當(dāng)程序出現(xiàn)異常的時候才會執(zhí)行該通知,寫法如下,就是在切面類中實(shí)現(xiàn)一個方法,使用@AfterThrowing注解修飾即可:
//拋異常后通知@AfterThrowing("pointcut()")public void doAfterThrowing() {System.out.println("拋出異常后,執(zhí)行AfterThrowing通知");}
然后我們再在目標(biāo)方法中構(gòu)造一個異常,異常隨便寫一個異常就行,如算術(shù)異常:
我們訪問頁面http://127.0.0.1:8080/user/world
來看一看控制臺輸出:
由于出現(xiàn)了異常,方法被強(qiáng)制終止了,沒有返回,所以沒有返回后通知。
最后還剩下一個環(huán)繞通知,環(huán)繞通知你可以理解為將前置通知和后置通知一體化了,環(huán)繞通知最常見的用法之一就是計算目標(biāo)方法執(zhí)行的時間是多少,使用其他通知無法做到,如果使用前置加后置通知進(jìn)行對目標(biāo)方法的計時,在單線程下沒有問題,但是在多線程下有問題,當(dāng)一個線程正在計時時,另外一個線程調(diào)用了前置通知,此時計時開始的時間就被刷新了,那自然計算得到的目標(biāo)方法執(zhí)行時間也就不準(zhǔn)確了,而環(huán)繞通知使一體化的,不存在類似這種線程安全的問題。
環(huán)繞通知相比于其他的三種通知的使用方法較為復(fù)雜,首先實(shí)現(xiàn)環(huán)繞通知的方法必須含有ProceedingJoinPoint
類的參數(shù)和使用 @Around注解修飾,表示連接點(diǎn)的執(zhí)行進(jìn)度,方法體里面第一步是執(zhí)行環(huán)繞方法的前置通知,然后通過該類對象獲取目標(biāo)方法執(zhí)行進(jìn)度并調(diào)用,再執(zhí)行環(huán)繞通知的后置通知,最后并返回該目標(biāo)方法進(jìn)度。
@Around("pointcut()")public Object doAround(ProceedingJoinPoint joinPoint) {Object res = null;System.out.println("執(zhí)行環(huán)繞通知前置通知");try {//根據(jù)連接點(diǎn)進(jìn)度獲取目標(biāo)方法,并執(zhí)行目標(biāo)方法res = joinPoint.proceed();} catch (Throwable e) {e.printStackTrace();}System.out.println("環(huán)繞通知后置通知");return res;}
我們訪問頁面http://127.0.0.1:8080/user/hello
來看一看控制臺輸出:
我們可以基于環(huán)繞通知實(shí)現(xiàn)對目標(biāo)方法的計時功能:
實(shí)現(xiàn)思路很簡單,就是在執(zhí)行目標(biāo)方法之前開始計時,執(zhí)行完目標(biāo)方法之后結(jié)束計時,差值就是方法運(yùn)行的時間。
計時的方式可以使用時間戳或者spring中的StopWatch
類,后者更準(zhǔn)確一點(diǎn),其實(shí)都差不多。
我們可以通過傳入的joinPoint
對象獲取目標(biāo)方法的方法名以及具體所在類和包,joinPoint.getSignature().toString()
就能生成目標(biāo)方法的全部有關(guān)名字的信息,我們可以加上一個方法的信息來表示哪一個方法執(zhí)行的時間。
@Around("pointcut()")public Object doTime(ProceedingJoinPoint joinPoint) {Object result = null;//System.out.println("環(huán)繞通知前置通知");String methodName = "";long start = 0;long end = 0;StopWatch stopWatch = new StopWatch();try {//執(zhí)行攔截方法start = System.currentTimeMillis();stopWatch.start();methodName = joinPoint.getSignature().toString();result = joinPoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();} finally {end = System.currentTimeMillis();stopWatch.stop();}//System.out.println("環(huán)繞通知后置通知");System.out.println(methodName + "執(zhí)行了" + (end - start) + "ms");System.out.println(methodName + "執(zhí)行了" + (stopWatch.getTotalTimeMillis()) + "ms");return result;}
運(yùn)行結(jié)果:
2.4Spring AOP的實(shí)現(xiàn)原理
Spring AOP是構(gòu)建在動態(tài)代理基礎(chǔ)上,因此 Spring對AOP的支持局限于方法級別的攔截。
Spring AOP支持JDKProxy 和CGLIBProxy方式實(shí)現(xiàn)動態(tài)代理。默認(rèn)情況下,對于非final
修飾的類,SpringAOP會基于CGLIBProxy生成代理類,CGLIBProxy生成代理類的原理就是繼承目標(biāo)類,被關(guān)鍵字final
修飾的類,由于不能被繼承,所以會基于DKProxy生成代理類。
SpringAOP的本質(zhì)就是生成一個目標(biāo)對象的代理類,當(dāng)前端傳來請求時,不會將請求直接交給目標(biāo)對象,而是首先代理類進(jìn)行處理,如果滿足一定的條件,才會將請求交給目標(biāo)對象。
如果處理請求前需要登錄驗(yàn)證,那么代理類會去驗(yàn)證用戶賬戶是否登錄,如果用戶登錄了才會將請求交給目標(biāo)對象并執(zhí)行核心業(yè)務(wù)代碼,否則代理類之間返回響應(yīng)讓用戶先登錄。
參考 & 資料
Spring3:AOP