手繪六張圖徹底搞懂動態代理
本文轉載自微信公眾號「愛笑的架構師」,作者雷小帥。轉載本文請聯系愛笑的架構師公眾號。
在講解動態代理前我們先聊聊什么是靜態代理。
靜態代理
假設有一天領導突發奇想,給你下發了一個需求:
統計項目中所有類的方法執行耗時。
在拿到需求的那一刻,腦海中冒出來的第一個想法是:
在每個方法的第一行和最后一行加上時間埋點,再打印一行日志不就完事了。
抄起鍵盤準備開干,想了想又開始猶豫了:
在每個方法都加幾行代碼,這不是侵入式修改嗎?
聽架構師大佬說這樣的場景可以用代理模式,那嘗試一下,具體做法如下。
靜態代理的實現
(1)為工程里每個類都寫一個代理類,讓它與目標類實現同一個接口。圖中標紅色的就是代理類。
(2)在代理類里面維護一個目標實現類,調用代理類的方法時還是會去調用目標類的方法,只不過在前后加了一些其他邏輯代碼。也就是說后面客戶端不需要直接調用目標實現類,只需要調用代理類即可,這樣就間接調用了對應方法。
用一個公式總結一下:代理類 = 增強代碼 + 目標實現類 。
下面這個圖中,計算耗時的邏輯就是增強代碼。
(3)在所有 new 目標類的地方都替換為 new 代理類,并將目標類作為構造方法參數傳入;所有使用目標類調用的地方全部都替換為代理類調用。
如果你看懂了上面的實現方法,那么恭喜你已經掌握了靜態代理的核心思想。
靜態代理的缺點
靜態代理的思路非常簡單,就是給每一個目標實現類寫一個對應的代理實現類,但是如果一個項目有幾千甚至有幾萬個類,這個工作量可想而知。
前面我們還隱藏了一個假設:每個類都會實現一個接口。那如果一個類沒有實現任何接口,代理類如何實現呢?
好了,我們來總結一下靜態代理的缺點:
- 靜態代理需要針對每個目標實現類寫一個對應的代理類,如果目標類的方法有變動,代理類也要跟著動,維護成本非常高。
- 靜態代理必須依賴接口。
既然知道了靜態代理的缺點,那有沒有辦法實現少些或者不寫代理類來實現代理功能呢?答案是有,動態代理。
對象的創建流程
在正式介紹動態代理前,我們先復習一下 java 中對象是如何創建的。
我們在項目中使用一行代碼就可以簡單創建一個對象,實際上經過的流程還是很復雜的。
// 創建對象
A a = new A();
- (1)java 源文件經過編譯生成字節碼文件(.class結尾);
- (2)類加載器將 class 文件加載到 JVM 內存中,就是常說的方法區,生成 Class 對象;
- (3)執行 new,申請一塊內存區域,緊接著創建一個對象放在 JVM 對象,準確地說是新生代;
上面的流程中提到了 Class 對象,有兩個概念初學者很容易混淆:Class 對象 和 實例對象。
Class 對象簡單來說就是 Class 類的實例,Class 類描述了所有的類;實例對象是通過 Class 對象創建出來的。
從上面的分析可以看出來,要想創建一個實例,最最關鍵的是獲得 Class 對象。
有些同學可能有疑問了,我寫代碼的時候創建對象沒有用到 Class 對象呀,那是因為 Java 語言底層幫你封裝了細節。Java 語言給我們提供了new 這個關鍵字,new 實在太好用了,一行代碼就可以創建一個對象。
我們再回到前面講的靜態代理,靜態代理最重要的是提前寫一個代理類,有了代理類就可以 new 一個代理對象。但是每次都去寫一個代理類是不是太麻煩了?!
再稍微擴展一下思路,有沒有辦法不寫代理類還能生成一個代理對象呢?可以,上面講的通過代理類 Class 對象就可以生成代理對象,那如何獲取代理類 Class 對象呢?我們接著往下看。
動態代理
Class對象包含了一個類的所有信息,如:構造方法、成員方法、成員屬性等。
如果我們不寫代理類,似乎無法獲得代理類 Class 對象,但稍稍動一動腦:代理類和目標類實現的是同一組接口,是不是可以通過接口間接獲得代理類 Class 對象。
代理類和目標類實現了同一組接口,這就說明他們大體結構都是一致的,這樣我們對代理對象的操作都可以轉移到目標對象身上,代理對象只需要專注于增強代碼的實現。
上面說了這么多其實是在引入動態代理的概念,動態代理相對于靜態代理最大的區別就是不需要事先寫好代理類,一般在程序的運行過程中動態產生代理類對象。
動態代理實現之 JDK
JDK 原生提供了動態代理的實現,主要是通過java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler這兩個類配合使用。
Proxy類有個靜態方法,傳入類加載器和一組接口就可以返回代理 Class 對象。
public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
這個方法的作用簡單來說就是,會將你傳入一組接口類的結構信息"拷貝"到一個新的 Class 對象中,新的 Class對象帶有構造器是可以創建對象的。
一句話總結:Proxy.getProxyClass() 這個靜態方法的本質是以 Class 造 Class。
拿到了 Class 對象,就可以使用反射創建實例對象了:
// Proxy.getProxyClass 默認會生成一個帶參數的構造方法,這里指定參數獲取構造方法
Constructor<A> constructor = aClazz.getConstructor(InvocationHandler.class);
// 使用反射創建代理對象
A a1 = constructor.newInstance(new InvocationHandler() {});
眼尖的同學已經看到了,創建實例的時候需要傳入一個 InvocationHandler 對象,說明代理對象中必然有一個成員變量去接收。在調用代理對象的方法時實際上會去執行 InvocationHandler 對象的 invoke方法,畫個圖理解一下:
invoke 方法里可以寫增強代碼,然后調用目標對象 work 方法。
總結一下流程:
(1)通過 Proxy.getProxyClass() 方法獲取代理類 Class 對象;
(2)通過反射 aClazz.getConstructor() 獲取構造器對象;
(3)定義InvocationHandler類并實例化,當然也可以直接使用匿名內部類;
(4)通過反射 constructor.newInstance() 創建代理類對象;
(5)調用代理方法;
看了上面的流程,是不是覺得比靜態代理還要繁瑣,有沒有更加優雅的方法?當然有!
為了盡量簡化操作,JDK Proxy 類直接提供了一個靜態方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
這個方法傳入類加載器、一組接口和 InvocationHandler 對象直接就可以返回代理對象了,有了代理對象就可以調用代理方法了,是不是 so easy?!
newProxyInstance方法本質上幫我們省略了獲取代理類對象和通過代理類對象創建代理類的過程,這些細節全部隱藏了。
所以真正在項目中直接使用newProxyInstance這個方法就好了,上面講的那些流程是為了方便大家理解整個過程。
看到這里我相信大家應該能看懂JDK 原生動態代理了。
動態代理實現之 cglib
JDK 動態代理,一旦目標類有了明確的接口,完全可以通過接口生成一個代理 Class 對象,通過代理 Class 對象就可以創建代理對象。
這里可以看出 JDK 動態代理有個限制必須要求目標類實現了接口,那加入一個目標類沒有實現接口,那豈不是不能使用動態代理了?
cglib 就是為了實現這個目標而出現的,利用asm開源包對代理對象類的class文件加載進來,通過修改其字節碼生成子類來處理。
JDK動態代理與 cglib 動態代理對比
我們通過幾個問題簡單對比一下 JDK 和 cglib 動態代理的區別。
問題 1:cglib 和 JDK 動態代理的區別?
JDK 動態代理:利用 InvocationHandler 加上反射機制生成一個代理接口的匿名類,在調用具體方法前調用InvokeHandler來處理
cglib 動態代理:利用ASM框架,將目標對象類生成的class文件加載進來,通過修改其字節碼生成代理子類
問題 2:cglib 比 JDK快?
cglib底層是ASM字節碼生成框架,在 JDK 1.6 前字節碼生成要比反射的效率高
在 JDK 1.6 之后 JDK 逐步對動態代理進行了優化,在 1.8 的時候 JDK 的效率已經高于 cglib
問題 3:Spring框架什么時候用 cglib 什么時候用 JDK 動態代理?
目標對象生成了接口默認用 JDK 動態代理
如果目標對象沒有實現接口,必須采用cglib
當然如果目標對象使用了接口也可以強制使用cglib
小結
使用代理模式可以避免侵入式修改原有代碼。代理分為:靜態代理和動態代理。
靜態代理要求目標類必須實現接口,通過新建代理類并且與目標類實現同一組接口,最終實現通過代理類間接調用目標類的方法。
關于代理類,可以用一個公式總結一下:代理類 = 增強代碼 + 目標實現類 。
靜態代理必須要求提前寫好代理類,使用起來比較繁瑣,這就引入了動態代理。
動態代理是在程序運行的過程中動態生成代理類,根據實現方式的不同進而分為:JDK原生動態代理和CGLIB動態代理。
JDK 動態代理通過反射+InvocationHandler 機制動態生成代理類來實現,要求目標類必須實現接口。cglib 不要求目標類實現接口,通過修改字節碼方式生成目標類的子類,這就是代理類。
動態代理不僅在 RPC 框架中被使用,還在其他地方有著廣泛的應用場景,比如:Spring AOP、測試框架 mock、用戶鑒權、日志、全局異常處理、事務處理等。
大家學會了嗎?