前言
本文將從另一個角度講解 AOP,從宏觀的實現(xiàn)原理和設計本質入手。大部分講 AOP 的博文都是一上來就羅列語法,然后敲個應用 demo就完了 。但學習不能知其然,不知其所以然。
對 AOP 我提出了幾點思考:AspectJ 為什么會大熱?AspectJ 是怎樣工作的?和 Spring AOP 有什么區(qū)別?什么場景下適用?我們能不能自己實現(xiàn)一個 AOP 方法?
在熟悉原理前,如果想先掌握 AOP 的使用方法可以看:
一、引入
敲一個小 Demo 來引入主題,假設我想不依賴任何 AOP 方法,在特定方法的執(zhí)行前后加上日志打印。
第一種方式:寫死代碼
定義一個目標類接口


把 before() 和 after() 方法寫死在 execute() 方法體中,非常不優(yōu)雅,我們改進一下。
第二種方式:靜態(tài)代理

但是存在一個問題,隨著打印日志的需求增多,Proxy 類越來越多,我們能不能保持只有一個代理呢?這時候我們就需要用到 JDK 動態(tài)代理了。
第三種方式:動態(tài)代理
新建動態(tài)代理類

客戶端調用

這又引出一個問題,日志打印和業(yè)務邏輯耦合在一起,我們希望把前置和后置抽離出來,作為單獨的增強類。
第四種方式:動態(tài)代理 + 分離增強類
新建增強類接口和實現(xiàn)類

用反射代替寫死方法,解耦代理和操作者

客戶端調用

但是用了反射性能太差了,而且動態(tài)代理用起來也不方便,有沒有更好的辦法?
我們發(fā)現(xiàn) Demo 存在種種問題
- 靜態(tài)代理每次都要自己新建個代理類,太繁瑣,重用性又差,一個代理不能同時代理多種類;
- 動態(tài)代理可以重用,但性能太差;
- 代理類耦合進被代理類的調用階段,萬一我需要改下 before、after 的方法名,可能會點燃一個炸彈;
- 代理攔截了一個類,就會攔截這個類的所有方法,難道我還要在代理類里加個 if-else 判斷特定方法過濾攔截?我們可以不可以只攔截特定的方法?
- 如果我既要打印日志,又要計算方法執(zhí)行用時,每次都要去改增強類嗎?
我們的訴求很簡單:1. 性能高;2. 松耦合;3. 步驟方便;4. 靈活性高。
那主流的 AOP 框架是怎么解決這個問題的呢?我們趕緊來看看!
二、AOP 方法
不同的 AOP 方法原理略微有些不同,我們先看下 AOP 實現(xiàn)方式有哪些:
| AOP方式 | 機制 | 說明 |
|---|---|---|
| 靜態(tài)織入 | 靜態(tài)代理 | 直接修改原類,比如編譯期生成代理類的 APT |
| 靜態(tài)織入 | 自定義類加載器 | 使用類加載器啟動自定義的類加載器,并加一個類加載監(jiān)聽器,監(jiān)聽器發(fā)現(xiàn)目標類被加載時就織入切入邏輯,以 Javassist 為代表 |
| 動態(tài)織入 | 動態(tài)代理 | 字節(jié)碼加載后,為接口動態(tài)生成代理類,將切面植入到代理類中,以 JDK Proxy 為代表 |
| 動態(tài)織入 | 動態(tài)字節(jié)碼生成 | 字節(jié)碼加載后,通過字節(jié)碼技術為一個類創(chuàng)建子類,并在子類中采用方法攔截的技術攔截所有父類方法的調用織入邏輯。屬于子類代理,以 CGLIB 為代表 |
所有 AOP 方法本質就是:攔截、代理、反射(動態(tài)情況下),實現(xiàn)原理可以看作是代理 / 裝飾設計模式的泛化,為什么這么說?我們來詳細分析一下。
三、靜態(tài)織入原理,以 AspectJ 為例
靜態(tài)織入原理就是靜態(tài)代理,我們以 AspectJ 為例。
1. AspectJ 設計思路
前面說到 Demo 存在的種種問題,AspectJ 是怎么解決的呢?AspectJ 提供了兩套強大的機制:
(1)切面語法 | 解決業(yè)務和切面的耦合
AspectJ 中的切面,就解決了這個問題。
@Before("execution(* android.view.View.OnClickListener.onClick(..))")
我們可以通過切面,將增強類與攔截匹配條件(切點)組合在一起,從而生成代理。這把是否要使用切面的決定權利還給了切面,我們在寫切面時就可以決定哪些類的哪些方法會被代理,從而邏輯上不需要侵入業(yè)務代碼。
而普通的代理模式并沒有做到切面與業(yè)務代碼的解耦,雖然將切面的邏輯獨立進了代理類,但是決定是否使用切面的權利仍然在業(yè)務代碼中。這才導致了 Demo 中種種的麻煩。
AspectJ 提供了兩套對切面的描述方法:
- 我們常用的基于 java 注解切面描述的方法,寫起來十分方便,兼容 Java 語法;
@Aspect
public class AnnoAspect {
@Pointcut("execution(...)")
public void jointPoint() {
}
@Before("jointPoint()")
public void before() {
//...
}
@After("jointPoint()")
public void after() {
//...
}
}
- 基于 aspect 文件的切面描述方法,這種語法不兼容 Java 語法。
public aspect AnnoAspect {
pointcut XX():
execution(...);
before(): XX() {
//...
}
after(): XX() {
//...
}
}
(2)織入工具 | 解決代理手動調用的繁瑣
那么切面語法讓切面從邏輯上與業(yè)務代碼解耦,但是我要怎么找到特定的業(yè)務代碼織入切面呢?
兩種解決思路:一種就是提供注冊機制,通過額外的配置文件指明哪些類受到切面的影響,不過這還是需要干涉對象創(chuàng)建的過程;另外一種解決思路就是在編譯期或類加載期先掃描切面,并將切面代碼通過某種形式插入到業(yè)務代碼中。
那 AspectJ 織入方式有兩種:一種是 ajc 編譯,可以在編譯期將切面織入到業(yè)務代碼中。另一種就是 aspectjweaver.jar 的 agent 代理,提供了一個 Java agent 用于在類加載期間織入切面。
2. 通過 class 反推 AspectJ 實現(xiàn)機制
(1)@Before 機制
國際慣例寫個 Demo
- 自定義 AutoLog 注解

- 編寫 LogAspect 切面

- 在切入點中加上注解

反編譯后(請點開大圖查看)

發(fā)現(xiàn) AspectJ 會把調用切面的方法插入到切入點中,且封裝了切入點所在的方法名、所在類、入?yún)⒚?、入?yún)⒅?、返回值等等信息,傳遞給切面,這樣就建立了切面和業(yè)務代碼的關聯(lián)。
我們跟進 LogAspect.aspectOf().aroundJoinPoint(localJoinPoint); 一探究竟。

我們發(fā)現(xiàn)了什么?其實 Before 和 After 的插入就是在匹配到的 JoinPoint 調用前后插入 Advise 方法,以此來達到攔截目標 JoinPoint 的作用。 如下圖所示:

(2)@Around 機制
- 自定義 SingleClick 注解

- 編寫 SingleClickAspect 切面

- 業(yè)務方加上注解

打開編譯后的 class 文件(請點開大圖查看)

我們發(fā)現(xiàn)和 Before、After 織入不一樣了!前者的織入只是在匹配的 JoinPoint 前后插入 Advise 方法,僅僅是插入。而 Around 拆分了業(yè)務代碼和 Advise 方法,把業(yè)務代碼遷移到新函數(shù)中,通過一個單獨的閉包拆分來執(zhí)行,相當于對目標 JoinPoint 進行了一個代理,所以 Around 情況下我們除了編寫切面邏輯,還需要手動調用 joinPoint.proceed() 來調用閉包執(zhí)行原方法。
我們看下 proceed() 都做了些什么

那這個 arc 是什么?什么時候拿到的呢?

繼續(xù)回溯

在 AroundClosure 閉包中,會把運行時對象和當前連接點 joinPoint 對象傳入,調用 linkClosureAndJoinPoint() 綁定兩端,這樣在 Around 中就可以通過 ProceedingJoinPoint.proceed() 調用 AroundClosure,進而調用到目標方法了。
那么一圖總結 Around 機制:

我們從 AspectJ 編譯后的 class 文件可以明顯看出執(zhí)行的邏輯,proceed 方法就是回調執(zhí)行被代理類中的方法。
所以 AspectJ 做的事情如下:
首先從文件列表里取出所有的文件名,讀取文件,進行分析;
掃描含有 aspect 的切面文件;
根據(jù)切面中定義規(guī)則,攔截匹配的 JoinPoint ;
繼續(xù)讀取切面定義的規(guī)則,根據(jù) around 或 before ,采用不同策略織入切面。
(3)@Before @After 機制與 @Around 機制區(qū)別
- Before、After 僅僅是織入了 Advise 方法
- Around 使用了代理 + 閉包的方式進行替換
3. AspectJ 底層技術總結
分析完 class 你會發(fā)現(xiàn),AspectJ 實際上就是用一種特定語言編寫切面,通過自己的語法編譯工具 ajc 編譯器來編譯,生成一個新的代理類,該代理類增強了業(yè)務類。
-
AspectJ 就是一個代碼生成工具;
編寫一段通用的代碼,然后根據(jù) AspectJ 語法定義一套代碼生成規(guī)則,AspectJ 就會幫你把這段代碼插入到對應的位置去。
-
AspectJ 語法就是用來定義代碼生成規(guī)則的語法。
擴展編譯器,引入特定的語法來創(chuàng)建 Advise,從而在編譯期間就織入了Advise 的代碼。
如果使用過 Java Compiler Compiler (JavaCC),你會發(fā)現(xiàn)兩者的代碼生成規(guī)則的理念驚人相似。JavaCC 允許你在語法定義規(guī)則文件中,加入你自己的 Java 代碼,用來處理讀入的各種語法元素。
四、動態(tài)織入原理,以 Spring AOP 為例
動態(tài)織入原理就是動態(tài)代理。
1. Spring AOP 執(zhí)行原理
Spring AOP 利用截取的方式,對被代理類進行裝飾,以取代原有對象行為的執(zhí)行,不會生成新類。
2. Spring AOP VS AspectJ
可能有的小伙伴會困惑了,Spring AOP 使用了 AspectJ,怎么是動態(tài)代理呢?
那是因為 Spring 只是使用了與 AspectJ 一樣的注解,沒有使用 AspectJ 的編譯器,轉向采用動態(tài)代理技術的實現(xiàn)原理來構建 Spring AOP 的內部機制(動態(tài)織入),這是與 AspectJ(靜態(tài)織入)最根本的區(qū)別。
Spring 底層的動態(tài)代理分為兩種 JDK 動態(tài)代理和 CGLib:
JDK 動態(tài)代理用于對接口的代理,動態(tài)產生一個實現(xiàn)指定接口的類,注意動態(tài)代理有個約束:目標對象一定是要有接口的,沒有接口就不能實現(xiàn)動態(tài)代理,只能為接口創(chuàng)建動態(tài)代理實例,而不能對類創(chuàng)建動態(tài)代理。
CGLIB 用于對類的代理,把被代理對象類的 class 文件加載進來,修改其字節(jié)碼生成一個繼承了被代理類的子類。使用 cglib 就是為了彌補動態(tài)代理的不足。
3. JDK 動態(tài)代理的原理
我們前面的 Demo 第三種方式使用了動態(tài)代理,我們不禁有了疑問,動態(tài)代理類及其對象實例是如何生成的?調用動態(tài)代理對象方法為什么可以調用到目標對象方法?

我們通過 Proxy.newProxyInstance 可以動態(tài)生成指定接口的代理類的實例。我們來看下newProxyInstance內部實現(xiàn)機制。

代理對象會實現(xiàn)接口的所有方法,實現(xiàn)的方法交由我們自定義的 handler 來處理。

我們看下 getProxyClass0 方法,只憑一個類加載器、一個接口,是怎么創(chuàng)建代理類的?

注意一下:Android 中動態(tài)代理類是直接生成,而 Java 是生成代理類的字節(jié)碼,再根據(jù)字節(jié)碼生成代理類。
那么客戶端就可以 getProxy() 拿到生成的代理類 com.sun.proxy.$Proxy0

這個代理類繼承自 Proxy 并實現(xiàn)了我們被代理類的所有接口,在各個接口方法的內部,通過反射調用了 InvocationHandlerImpl 的 invoke 方法。
總結下步驟:
- 獲得被代理類的接口信息,生成一個實現(xiàn)了代理接口的動態(tài)代理類;
- 通過反射獲得代理類的構造函數(shù);
- 利用構造函數(shù)生成動態(tài)代理類的實例對象,在調用具體方法前調用 invokeHandler 方法來處理。

后記
1. 設計模式不能脫離業(yè)務場景
不知不覺我們復習了一下代理模式,設計模式必須依賴大量的業(yè)務場景,脫離業(yè)務去看設計模式是沒有意義的。
因為脫離了應用場景,即使理解了模式的內容和結構,也學不會在合適的時候應用。
2. 敢于追求優(yōu)雅的代碼
首先你要敢于追求優(yōu)雅的代碼,就像我們開頭的打印日志的需求,不斷提出問題,不斷追求更好的解決方案,在新的方案上挖掘新的問題……如果你完全不追求設計,那自然是不會想到去研究設計模式的。
本篇完成耗時 26 個番茄鐘(650 分鐘)
我是 FeelsChaotic,一個寫得了代碼 p 得了圖,剪得了視頻畫得了畫的程序媛,致力于追求代碼優(yōu)雅、架構設計和 T 型成長。
歡迎關注 FeelsChaotic 的簡書和掘金,如果我的文章對你哪怕有一點點幫助,歡迎 ??!你的鼓勵是我寫作的最大動力!
最最重要的,請給出你的建議或意見,有錯誤請多多指正!