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