點擊上方“Java知音”,選擇“置頂公眾號”
技術文章第一時間送達!
作者:FeelsChaotic
juejin.im/post/5c57b2d5e51d457ffd56ffbb
前言
本文將從另一個角度講解 AOP,從宏觀的實現原理和設計本質入手。大部分講 AOP 的博文都是一上來就羅列語法,然后敲個應用 demo就完了 。但學習不能知其然,不知其所以然。
對 AOP 我提出了幾點思考:
AspectJ 為什么會大熱?
AspectJ 是怎樣工作的?
和 Spring AOP 有什么區別?
什么場景下適用我們能不能自己實現一個 AOP 方法?
一、引入
敲一個小 Demo 來引入主題,假設我想不依賴任何 AOP 方法,在特定方法的執行前后加上日志打印。
第一種方式:寫死代碼
定義一個目標類接口
把 before() 和 after() 方法寫死在 execute() 方法體中,非常不優雅,我們改進一下。
第二種方式:靜態代理
但是存在一個問題,隨著打印日志的需求增多,Proxy 類越來越多,我們能不能保持只有一個代理呢?這時候我們就需要用到 JDK 動態代理了。
第三種方式:動態代理
新建動態代理類
客戶端調用
這又引出一個問題,日志打印和業務邏輯耦合在一起,我們希望把前置和后置抽離出來,作為單獨的增強類。
第四種方式:動態代理 + 分離增強類
新建增強類接口和實現類
用反射代替寫死方法,解耦代理和操作者
客戶端調用
但是用了反射性能太差了,而且動態代理用起來也不方便,有沒有更好的辦法?
我們發現 Demo 存在種種問題
靜態代理每次都要自己新建個代理類,太繁瑣,重用性又差,一個代理不能同時代理多種類;
動態代理可以重用,但性能太差;
代理類耦合進被代理類的調用階段,萬一我需要改下 before、after 的方法名,可能會點燃一個炸彈;
代理攔截了一個類,就會攔截這個類的所有方法,難道我還要在代理類里加個 if-else 判斷特定方法過濾攔截?我們可以不可以只攔截特定的方法?
如果我既要打印日志,又要計算方法執行用時,每次都要去改增強類嗎?
我們的訴求很簡單:1. 性能高;2. 松耦合;3. 步驟方便;4. 靈活性高。
那主流的 AOP 框架是怎么解決這個問題的呢?我們趕緊來看看!
二、AOP 方法
不同的 AOP 方法原理略微有些不同,我們先看下 AOP 實現方式有哪些:
所有 AOP 方法本質就是:攔截、代理、反射(動態情況下),實現原理可以看作是代理 / 裝飾設計模式的泛化,為什么這么說?我們來詳細分析一下。Java:由淺入深揭開 AOP 實現原理
三、靜態織入原理,以 AspectJ 為例
靜態織入原理就是靜態代理,我們以 AspectJ 為例。
1. AspectJ 設計思路
前面說到 Demo 存在的種種問題,AspectJ 是怎么解決的呢?AspectJ 提供了兩套強大的機制:
(1)切面語法 | 解決業務和切面的耦合
AspectJ 中的切面,就解決了這個問題。
@Before("execution(*?android.view.View.OnClickListener.onClick(..))")
我們可以通過切面,將增強類與攔截匹配條件(切點)組合在一起,從而生成代理。這把是否要使用切面的決定權利還給了切面,我們在寫切面時就可以決定哪些類的哪些方法會被代理,從而邏輯上不需要侵入業務代碼。
而普通的代理模式并沒有做到切面與業務代碼的解耦,雖然將切面的邏輯獨立進了代理類,但是決定是否使用切面的權利仍然在業務代碼中。這才導致了 Demo 中種種的麻煩。
AspectJ 提供了兩套對切面的描述方法:
1.我們常用的基于 java 注解切面描述的方法,寫起來十分方便,兼容 Java 語法;
@Aspect
public?class?AnnoAspect?{
????@Pointcut("execution(...)")
????public?void?jointPoint()?{
????}
????@Before("jointPoint()")
????public?void?before()?{
????????//...
????}
????@After("jointPoint()")
????public?void?after()?{
????????//...
????}
}
2.基于 aspect 文件的切面描述方法,這種語法不兼容 Java 語法。
public?aspect?AnnoAspect?{
????pointcut?XX():execution(...);
????before():?XX()?{
????????//...
????}
????after():?XX()?{
????????//...
????}
}????
(2)織入工具 | 解決代理手動調用的繁瑣
那么切面語法讓切面從邏輯上與業務代碼解耦,但是我要怎么找到特定的業務代碼織入切面呢?
兩種解決思路:一種就是提供注冊機制,通過額外的配置文件指明哪些類受到切面的影響,不過這還是需要干涉對象創建的過程;另外一種解決思路就是在編譯期或類加載期先掃描切面,并將切面代碼通過某種形式插入到業務代碼中。
那 AspectJ 織入方式有兩種:一種是 ajc 編譯,可以在編譯期將切面織入到業務代碼中。另一種就是 aspectjweaver.jar 的 agent 代理,提供了一個 Java agent 用于在類加載期間織入切面。
2. 通過 class 反推 AspectJ 實現機制
(1)@Before 機制
國際慣例寫個 Demo
1.自定義 AutoLog 注解
2.編寫 LogAspect 切面
3.在切入點中加上注解
反編譯后(請點開大圖查看)
發現 AspectJ 會把調用切面的方法插入到切入點中,且封裝了切入點所在的方法名、所在類、入參名、入參值、返回值等等信息,傳遞給切面,這樣就建立了切面和業務代碼的關聯。
我們跟進 LogAspect.aspectOf().aroundJoinPoint(localJoinPoint); 一探究竟。
我們發現了什么?其實 Before 和 After 的插入就是在匹配到的 JoinPoint 調用前后插入 Advise 方法,以此來達到攔截目標 JoinPoint 的作用。如下圖所示:
(2)@Around 機制
1.自定義 SingleClick 注解
2.編寫 SingleClickAspect 切面
3.業務方加上注解
打開編譯后的 class 文件(請點開大圖查看)
我們發現和 Before、After 織入不一樣了!前者的織入只是在匹配的 JoinPoint 前后插入 Advise 方法,僅僅是插入。而 Around 拆分了業務代碼和 Advise 方法,把業務代碼遷移到新函數中,通過一個單獨的閉包拆分來執行,相當于對目標 JoinPoint 進行了一個代理,所以 Around 情況下我們除了編寫切面邏輯,還需要手動調用 joinPoint.proceed() 來調用閉包執行原方法。
我們看下 proceed() 都做了些什么
那這個 arc 是什么?什么時候拿到的呢?
繼續回溯
在 AroundClosure 閉包中,會把運行時對象和當前連接點 joinPoint 對象傳入,調用 linkClosureAndJoinPoint() 綁定兩端,這樣在 Around 中就可以通過 ProceedingJoinPoint.proceed() 調用 AroundClosure,進而調用到目標方法了。
那么一圖總結 Around 機制:
我們從 AspectJ 編譯后的 class 文件可以明顯看出執行的邏輯,proceed 方法就是回調執行被代理類中的方法。
所以 AspectJ 做的事情如下:
首先從文件列表里取出所有的文件名,讀取文件,進行分析;
掃描含有 aspect 的切面文件;
根據切面中定義規則,攔截匹配的 JoinPoint ;
繼續讀取切面定義的規則,根據 around 或 before ,采用不同策略織入切面。
(3)@Before @After 機制與 @Around 機制區別
Before、After 僅僅是織入了 Advise 方法
Around 使用了代理 + 閉包的方式進行替換
3. AspectJ 底層技術總結
分析完 class 你會發現,AspectJ 實際上就是用一種特定語言編寫切面,通過自己的語法編譯工具 ajc 編譯器來編譯,生成一個新的代理類,該代理類增強了業務類。
AspectJ 就是一個代碼生成工具;
編寫一段通用的代碼,然后根據 AspectJ 語法定義一套代碼生成規則,AspectJ 就會幫你把這段代碼插入到對應的位置去。Java知音擴展:代碼神器:拒絕重復編碼,這款IDEA插件了解一下.....
AspectJ 語法就是用來定義代碼生成規則的語法。
擴展編譯器,引入特定的語法來創建 Advise,從而在編譯期間就織入了Advise 的代碼。
如果使用過 Java Compiler Compiler (JavaCC),你會發現兩者的代碼生成規則的理念驚人相似。JavaCC 允許你在語法定義規則文件中,加入你自己的 Java 代碼,用來處理讀入的各種語法元素。
四、動態織入原理,以 Spring AOP 為例
動態織入原理就是動態代理。
1. Spring AOP 執行原理
Spring AOP 利用截取的方式,對被代理類進行裝飾,以取代原有對象行為的執行,不會生成新類。
2. Spring AOP VS AspectJ
可能有的小伙伴會困惑了,Spring AOP 使用了 AspectJ,怎么是動態代理呢?
那是因為 Spring 只是使用了與 AspectJ 一樣的注解,沒有使用 AspectJ 的編譯器,轉向采用動態代理技術的實現原理來構建 Spring AOP 的內部機制(動態織入),這是與 AspectJ(靜態織入)最根本的區別。
Spring 底層的動態代理分為兩種 JDK 動態代理和 CGLib:
JDK 動態代理用于對接口的代理,動態產生一個實現指定接口的類,注意動態代理有個約束:目標對象一定是要有接口的,沒有接口就不能實現動態代理,只能為接口創建動態代理實例,而不能對類創建動態代理。
CGLIB 用于對類的代理,把被代理對象類的 class 文件加載進來,修改其字節碼生成一個繼承了被代理類的子類。使用 cglib 就是為了彌補動態代理的不足。
3. JDK 動態代理的原理
我們前面的 Demo 第三種方式使用了動態代理,我們不禁有了疑問,動態代理類及其對象實例是如何生成的?調用動態代理對象方法為什么可以調用到目標對象方法?
我們通過 Proxy.newProxyInstance 可以動態生成指定接口的代理類的實例。我們來看下newProxyInstance內部實現機制。
代理對象會實現接口的所有方法,實現的方法交由我們自定義的 handler 來處理。
我們看下 getProxyClass0 方法,只憑一個類加載器、一個接口,是怎么創建代理類的?
注意一下:Android 中動態代理類是直接生成,而 Java 是生成代理類的字節碼,再根據字節碼生成代理類。
那么客戶端就可以 getProxy() 拿到生成的代理類?com.sun.proxy.$Proxy0
這個代理類繼承自 Proxy 并實現了我們被代理類的所有接口,在各個接口方法的內部,通過反射調用了 InvocationHandlerImpl 的 invoke 方法。
總結下步驟:
獲得被代理類的接口信息,生成一個實現了代理接口的動態代理類;
通過反射獲得代理類的構造函數;
利用構造函數生成動態代理類的實例對象,在調用具體方法前調用 invokeHandler 方法來處理。
后記
1. 設計模式不能脫離業務場景
不知不覺我們復習了一下代理模式,設計模式必須依賴大量的業務場景,脫離業務去看設計模式是沒有意義的。
因為脫離了應用場景,即使理解了模式的內容和結構,也學不會在合適的時候應用。
設計模式推薦:設計模式內容聚合
2. 敢于追求優雅的代碼
首先你要敢于追求優雅的代碼,就像我們開頭的打印日志的需求,不斷提出問題,不斷追求更好的解決方案,在新的方案上挖掘新的問題……如果你完全不追求設計,那自然是不會想到去研究設計模式的。
END
Java面試題專欄
【41期】盤點那些必問的數據結構算法題之鏈表【42期】盤點那些必問的數據結構算法題之二叉堆【43期】盤點那些必問的數據結構算法題之二叉樹基礎【44期】盤點那些必問的數據結構算法題之二分查找算法【45期】盤點那些必問的數據結構算法題之基礎排序【46期】盤點那些必問的數據結構算法題之快速排序【47期】六大類二叉樹面試題匯總解答【48期】盤點Netty面試常問考點:什么是 Netty 的零拷貝?【49期】面試官:SpringMVC的控制器是單例的嗎?【50期】基礎考察:ClassNotFoundException 和 NoClassDefFoundError 有什么區別
我知道你 “在看”