本文將從另外一個角度講解 AOP,從宏觀的實現原理和設計本質入手。大部分講 AOP 的博文都是一上來就羅列語法,而後敲個應用 demo就完了 。但學習不能知其然,不知其因此然。java
對 AOP 我提出了幾點思考:AspectJ 爲何會大熱?AspectJ 是怎樣工做的?和 Spring AOP 有什麼區別?什麼場景下適用?咱們能不能本身實現一個 AOP 方法?android
在熟悉原理前,若是想先掌握 AOP 的使用方法能夠看:設計模式
敲一個小 Demo 來引入主題,假設我想不依賴任何 AOP 方法,在特定方法的執行先後加上日誌打印。bash
定義一個目標類接口閉包
把 before() 和 after() 方法寫死在 execute() 方法體中,很是不優雅,咱們改進一下。架構
可是存在一個問題,隨着打印日誌的需求增多,Proxy 類愈來愈多,咱們能不能保持只有一個代理呢?這時候咱們就須要用到 JDK 動態代理了。框架
新建動態代理類函數
客戶端調用工具
這又引出一個問題,日誌打印和業務邏輯耦合在一塊兒,咱們但願把前置和後置抽離出來,做爲單獨的加強類。post
新建加強類接口和實現類
用反射代替寫死方法,解耦代理和操做者
客戶端調用
可是用了反射性能太差了,並且動態代理用起來也不方便,有沒有更好的辦法?
咱們的訴求很簡單:1. 性能高;2. 鬆耦合;3. 步驟方便;4. 靈活性高。
那主流的 AOP 框架是怎麼解決這個問題的呢?咱們趕忙來看看!
不一樣的 AOP 方法原理略微有些不一樣,咱們先看下 AOP 實現方式有哪些:
AOP方式 | 機制 | 說明 |
---|---|---|
靜態織入 | 靜態代理 | 直接修改原類,好比編譯期生成代理類的 APT |
靜態織入 | 自定義類加載器 | 使用類加載器啓動自定義的類加載器,並加一個類加載監聽器,監聽器發現目標類被加載時就織入切入邏輯,以 Javassist 爲表明 |
動態織入 | 動態代理 | 字節碼加載後,爲接口動態生成代理類,將切面植入到代理類中,以 JDK Proxy 爲表明 |
動態織入 | 動態字節碼生成 | 字節碼加載後,經過字節碼技術爲一個類建立子類,並在子類中採用方法攔截的技術攔截全部父類方法的調用織入邏輯。屬於子類代理,以 CGLIB 爲表明 |
全部 AOP 方法本質就是:攔截、代理、反射(動態狀況下),實現原理能夠看做是代理 / 裝飾設計模式的泛化,爲何這麼說?咱們來詳細分析一下。
靜態織入原理就是靜態代理,咱們以 AspectJ 爲例。
前面說到 Demo 存在的種種問題,AspectJ 是怎麼解決的呢?AspectJ 提供了兩套強大的機制:
AspectJ 中的切面,就解決了這個問題。
@Before("execution(* android.view.View.OnClickListener.onClick(..))")
複製代碼
咱們能夠經過切面,將加強類與攔截匹配條件(切點)組合在一塊兒,從而生成代理。這把是否要使用切面的決定權利還給了切面,咱們在寫切面時就能夠決定哪些類的哪些方法會被代理,從而邏輯上不須要侵入業務代碼。
而普通的代理模式並無作到切面與業務代碼的解耦,雖然將切面的邏輯獨立進了代理類,可是決定是否使用切面的權利仍然在業務代碼中。這才致使了 Demo 中種種的麻煩。
AspectJ 提供了兩套對切面的描述方法:
@Aspect
public class AnnoAspect {
@Pointcut("execution(...)")
public void jointPoint() {
}
@Before("jointPoint()")
public void before() {
//...
}
@After("jointPoint()")
public void after() {
//...
}
}
複製代碼
public aspect AnnoAspect {
pointcut XX():
execution(...);
before(): XX() {
//...
}
after(): XX() {
//...
}
}
複製代碼
那麼切面語法讓切面從邏輯上與業務代碼解耦,可是我要怎麼找到特定的業務代碼織入切面呢?
兩種解決思路:一種就是提供註冊機制,經過額外的配置文件指明哪些類受到切面的影響,不過這仍是須要干涉對象建立的過程;另一種解決思路就是在編譯期或類加載期先掃描切面,並將切面代碼經過某種形式插入到業務代碼中。
那 AspectJ 織入方式有兩種:一種是 ajc 編譯,能夠在編譯期將切面織入到業務代碼中。另外一種就是 aspectjweaver.jar 的 agent 代理,提供了一個 Java agent 用於在類加載期間織入切面。
@Before
機制國際慣例寫個 Demo
反編譯後(請點開大圖查看)
發現 AspectJ 會把調用切面的方法插入到切入點中,且封裝了切入點所在的方法名、所在類、入參名、入參值、返回值等等信息,傳遞給切面,這樣就創建了切面和業務代碼的關聯。
咱們跟進 LogAspect.aspectOf().aroundJoinPoint(localJoinPoint);
一探究竟。
咱們發現了什麼?其實 Before 和 After 的插入就是在匹配到的 JoinPoint 調用先後插入 Advise 方法,以此來達到攔截目標 JoinPoint 的做用。 以下圖所示:
@Around
機制打開編譯後的 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 ,採用不一樣策略織入切面。
@Before
@After
機制與 @Around
機制區別分析完 class 你會發現,AspectJ 實際上就是用一種特定語言編寫切面,經過本身的語法編譯工具 ajc 編譯器來編譯,生成一個新的代理類,該代理類加強了業務類。
AspectJ 就是一個代碼生成工具;
編寫一段通用的代碼,而後根據 AspectJ 語法定義一套代碼生成規則,AspectJ 就會幫你把這段代碼插入到對應的位置去。
AspectJ 語法就是用來定義代碼生成規則的語法。
擴展編譯器,引入特定的語法來建立 Advise,從而在編譯期間就織入了Advise 的代碼。
若是使用過 Java Compiler Compiler (JavaCC),你會發現二者的代碼生成規則的理念驚人類似。JavaCC 容許你在語法定義規則文件中,加入你本身的 Java 代碼,用來處理讀入的各類語法元素。
動態織入原理就是動態代理。
Spring AOP 利用截取的方式,對被代理類進行裝飾,以取代原有對象行爲的執行,不會生成新類。
可能有的小夥伴會困惑了,Spring AOP 使用了 AspectJ,怎麼是動態代理呢?
那是由於 Spring 只是使用了與 AspectJ 同樣的註解,沒有使用 AspectJ 的編譯器,轉向採用動態代理技術的實現原理來構建 Spring AOP 的內部機制(動態織入),這是與 AspectJ(靜態織入)最根本的區別。
Spring 底層的動態代理分爲兩種 JDK 動態代理和 CGLib:
JDK 動態代理用於對接口的代理,動態產生一個實現指定接口的類,注意動態代理有個約束:目標對象必定是要有接口的,沒有接口就不能實現動態代理,只能爲接口建立動態代理實例,而不能對類建立動態代理。
CGLIB 用於對類的代理,把被代理對象類的 class 文件加載進來,修改其字節碼生成一個繼承了被代理類的子類。使用 cglib 就是爲了彌補動態代理的不足。
咱們前面的 Demo 第三種方式使用了動態代理,咱們不由有了疑問,動態代理類及其對象實例是如何生成的?調用動態代理對象方法爲何能夠調用到目標對象方法?
咱們經過 Proxy.newProxyInstance
能夠動態生成指定接口的代理類的實例。咱們來看下newProxyInstance
內部實現機制。
代理對象會實現接口的全部方法,實現的方法交由咱們自定義的 handler 來處理。
咱們看下 getProxyClass0
方法,只憑一個類加載器、一個接口,是怎麼建立代理類的?
注意一下:Android 中動態代理類是直接生成,而 Java 是生成代理類的字節碼,再根據字節碼生成代理類。
那麼客戶端就能夠 getProxy()
拿到生成的代理類 com.sun.proxy.$Proxy0
這個代理類繼承自 Proxy
並實現了咱們被代理類的全部接口,在各個接口方法的內部,經過反射調用了 InvocationHandlerImpl
的 invoke
方法。
總結下步驟:
不知不覺咱們複習了一下代理模式,設計模式必須依賴大量的業務場景,脫離業務去看設計模式是沒有意義的。
由於脫離了應用場景,即便理解了模式的內容和結構,也學不會在合適的時候應用。
首先你要勇於追求優雅的代碼,就像咱們開頭的打印日誌的需求,不斷提出問題,不斷追求更好的解決方案,在新的方案上挖掘新的問題……若是你徹底不追求設計,那天然是不會想到去研究設計模式的。
本篇完成耗時 26 個番茄鍾(650 分鐘)
我是 FeelsChaotic,一個寫得了代碼 p 得了圖,剪得了視頻畫得了畫的程序媛,致力於追求代碼優雅、架構設計和 T 型成長。
歡迎關注 FeelsChaotic 的簡書和掘金,若是個人文章對你哪怕有一點點幫助,歡迎 ❤️!你的鼓勵是我寫做的最大動力!
最最重要的,請給出你的建議或意見,有錯誤請多多指正!