深入講解基于JDK的動(dòng)態(tài)代理機(jī)制
前言
『動(dòng)態(tài)代理』其實(shí)源于設(shè)計(jì)模式中的代理模式,而代理模式就是使用代理對(duì)象完成用戶請(qǐng)求,屏蔽用戶對(duì)真實(shí)對(duì)象的訪問。
舉個(gè)最簡單的例子,比如我們想要「FQ」訪問國外網(wǎng)站,因?yàn)槲覀儾]有墻掉所有國外的 IP,所以你可以將你的請(qǐng)求數(shù)據(jù)報(bào)發(fā)送到那些沒有被屏蔽的國外主機(jī)上,然后你通過配置國外主機(jī)將請(qǐng)求轉(zhuǎn)發(fā)到目的地并在得到響應(yīng)報(bào)文后轉(zhuǎn)發(fā)回我們國內(nèi)主機(jī)上。
這個(gè)例子中,國外主機(jī)就是一個(gè)代理對(duì)象,而那些被墻掉的主機(jī)就是真實(shí)對(duì)象,我們不能直接訪問到真實(shí)對(duì)象,但可以通過一個(gè)代理間接的訪問到。
代理模式的一個(gè)好處就是,所有的外部請(qǐng)求都經(jīng)過代理對(duì)象,而代理對(duì)象有權(quán)利控制是否允許你真正的訪問到真實(shí)對(duì)象,如果不合法的請(qǐng)求,代理對(duì)象完全可以拒絕你而不用實(shí)際麻煩到真實(shí)對(duì)象。
代理模式的一個(gè)最典型的應(yīng)用就是 Spring 框架,Spring 的 AOP 以面向切面式編程將實(shí)際的業(yè)務(wù)邏輯和相關(guān)日志異常等信息隔離開,而你每次對(duì)業(yè)務(wù)邏輯的請(qǐng)求都對(duì)應(yīng)的是一個(gè)代理對(duì)象,這個(gè)代理對(duì)象中除了進(jìn)行必要的權(quán)限檢查,日志打印,就是真實(shí)的業(yè)務(wù)邏輯處理塊。
靜態(tài)代理
代理模式的實(shí)現(xiàn)者主要有兩種,『靜態(tài)代理』和『動(dòng)態(tài)代理』,這兩者的本質(zhì)區(qū)別就在于,前者的代理類是需要程序員手動(dòng)編碼的,而后者的代理類是自動(dòng)生成的。所以,這也是你幾乎沒有聽過『靜態(tài)代理』這個(gè)概念的原因,當(dāng)然,了解一下靜態(tài)代理自然更容易去理解『動(dòng)態(tài)代理』。
有一點(diǎn)大家需要清楚,代理對(duì)象代理了真實(shí)對(duì)象所有的方法,也就是代理對(duì)象需要向外提供至少和真實(shí)對(duì)象一樣的方法名供調(diào)用,所以一個(gè)代理對(duì)象就需要定義出真實(shí)對(duì)象擁有的所有方法,包括父類中的方法。
我們看一個(gè)簡單的靜態(tài)代理示例:
為了說明問題,我們定義了一個(gè) IService 接口,并讓我們的真實(shí)類繼承并實(shí)現(xiàn)該接口,這樣我們的真實(shí)類中就有兩個(gè)方法了。
那么代理類該怎樣定義才能完成對(duì)真實(shí)對(duì)象的代理呢?
一般來說,代理類的本質(zhì)就是,定義出真實(shí)類中所有的方法并在方法內(nèi)部添加一些其他操作,最后再調(diào)用真實(shí)類的該方法。
代理類要代理真實(shí)類中所有的方法,也就是說需要定義和真實(shí)類中那些方法簽名一模一樣的方法,而這些方法的內(nèi)部還是會(huì)間接調(diào)用真實(shí)類的該方法。
所以一般來說,代理類會(huì)選擇直接繼承真實(shí)類所有的接口和父類以便拿到真實(shí)類所有的父級(jí)方法簽名,也就是先代理所有的父級(jí)方法。
接著,代理真實(shí)類中非父級(jí)方法,以這里的例子來說,doService 方法就是真實(shí)類自己的方法,我們的代理類也要定義一個(gè)一模一樣方法簽名的方法對(duì)其進(jìn)行代理。
這樣,我們的代理類就算是完成了,以后對(duì)于真實(shí)類中所有方法的調(diào)用都可以通過代理類進(jìn)行代理。像這樣:
public static void main(String[] args){ realClass realClass = new realClass(); ProxyClass proxyClass = new ProxyClass(realClass); proxyClass.sayHello(); proxyClass.doService(); }
proxyClass 作為一個(gè)代理類對(duì)象,可以代理真實(shí)類中所有的方法,并在這些方法執(zhí)行之前,打印了一些「無關(guān)緊要」的信息。
代理模式的一個(gè)基本實(shí)現(xiàn)思路基本是這樣,但是動(dòng)態(tài)代理不同于這種靜態(tài)代理的一點(diǎn)在于,動(dòng)態(tài)代理不用我們一個(gè)一個(gè)方法的定義,虛擬機(jī)會(huì)自動(dòng)為你生成這些方法。
JDK 動(dòng)態(tài)代理機(jī)制
動(dòng)態(tài)代理區(qū)別于靜態(tài)代理的一點(diǎn)是,動(dòng)態(tài)代理的代理類由虛擬機(jī)在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建并于虛擬機(jī)卸載時(shí)清除。
我們復(fù)用上述靜態(tài)代理中使用的類,看看 JDK 的動(dòng)態(tài)代理具體是如何做到代理出某個(gè)類實(shí)例的所有方法的。
定義一個(gè) Handler 處理類:
Main 函數(shù)中調(diào)用 JDK 的動(dòng)態(tài)代理 API 生成代理類實(shí)例:
涉及的代碼還是比較多的,我們一點(diǎn)點(diǎn)來分析。首先,realClass 作為我們的被代理類實(shí)現(xiàn)了接口 IService 并在內(nèi)部定義了一個(gè)自己的方法 doService。
接著,我們定義了一個(gè)處理類,它繼承了接口 InvocationHandler 并實(shí)現(xiàn)了其唯一申明的 invoke 方法。除此之外,我們還得聲明一個(gè)成員字段用于存儲(chǔ)真實(shí)對(duì)象,也就是被代理對(duì)象,因?yàn)槲覀兇淼娜魏畏椒ɑ旧隙际腔谡鎸?shí)對(duì)象的相關(guān)方法的。
關(guān)于這個(gè) invoke 方法的作用以及各個(gè)形式參數(shù)的意義,待會(huì)我們反射代理類源碼的時(shí)候再做詳細(xì)的分析。
最后,定義好我們的處理類,基本上就可以進(jìn)行基于 JDK 的動(dòng)態(tài)代理了。核心的方法是 Proxy 類的 newProxyInstance 方法,該方法有三個(gè)參數(shù),其一是一個(gè)類加載器,其二是被代理類實(shí)現(xiàn)的所有接口集合,其三是我們自定義的處理器類。
虛擬機(jī)會(huì)在運(yùn)行時(shí)使用你提供的類加載器,將所有指定的接口類加載進(jìn)方法區(qū),然后反射讀取這些接口中的方法并結(jié)合處理器類生成一個(gè)代理類型。
最后一句話可能有點(diǎn)抽象,如何「結(jié)合處理器類生成一個(gè)代理類型」?這一點(diǎn)我們通過指定虛擬機(jī)啟動(dòng)參數(shù),讓它保存下來生成的代理類的 Class 文件。
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
我們通過第三方工具反編譯這個(gè) Class 文件,內(nèi)容比較多,我們拆分了分析:
首先,這個(gè)代理類的名字是很隨意的,一個(gè)程序中如果有多個(gè)代理類要生成,「$Proxy + 數(shù)字」就是它們的類名。
接著,你會(huì)注意到這個(gè)代理類繼承 Proxy 類和我們指定的接口 IService(之前如果指定多個(gè)接口,這里就會(huì)繼承多個(gè)接口)。
然后你會(huì)發(fā)現(xiàn),這個(gè)構(gòu)造器需要一個(gè) InvocationHandler 類型的參數(shù),并且構(gòu)造器的主體就是將這個(gè) InvocationHandler 實(shí)例傳遞到父類 Proxy 的對(duì)應(yīng)字段進(jìn)行保存,這也是為什么所有的代理類都必須使用 Proxy 作為父類的一個(gè)原因,就是為了公用父類中的 InvocationHandler 字段。后面我們會(huì)知道,這一個(gè)小小的設(shè)計(jì)將導(dǎo)致基于 JDK 的動(dòng)態(tài)代理存在一個(gè)致命性的缺點(diǎn),待會(huì)介紹。
這一塊內(nèi)容也算是代理類中較為重要的部分了,它將于虛擬機(jī)靜態(tài)初始化這個(gè)代理類的時(shí)候執(zhí)行。這一大段代碼就是完成反射出所有接口中方法的功能,所有被反射出來的方法都對(duì)應(yīng)一個(gè) Method 類型的字段進(jìn)行存儲(chǔ)。
除此之外,虛擬機(jī)還反射了 Object 中的三個(gè)常用方法,也就是說,代理類還會(huì)代理真實(shí)對(duì)象從 Object 那繼承來的這三個(gè)方法。
最后一部分我們看到的就是,虛擬機(jī)根據(jù)靜態(tài)初始化代碼塊反射出來所有待代理的方法,為它們生成代理的方法。
這些方法看起來好多代碼,其實(shí)就一行代碼,從父類 Proxy 中取出構(gòu)造實(shí)例化時(shí)存入的處理器類,并調(diào)用它的 invoke 方法。
方法的參數(shù)基本一樣,第一個(gè)參數(shù)是當(dāng)前代理類實(shí)例(事實(shí)證明這個(gè)參數(shù)傳過去并沒什么用),第二個(gè)參數(shù)是 Method 方法實(shí)例,第三個(gè)參數(shù)是方法的形式參數(shù)集合,如果沒有就是 null。
而這會(huì)兒我們?cè)賮砜纯串?dāng)初自定的處理器類:
所有的代理類方法內(nèi)部都會(huì)調(diào)用處理器類的 invoke 方法并傳入被代理類的當(dāng)前方法,而這個(gè) invoke 方法可以選擇去讓 method 正常被調(diào)用,也可以跳過 method 的調(diào)用,甚至可以在 method 真正被調(diào)用前后做一些額外的事情。
這,就是 JDK 動(dòng)態(tài)代理的核心思想,我們稍微總結(jié)一下整個(gè)調(diào)用流程。
首先,一個(gè)處理器類的定義是必不可少的,它的內(nèi)部必須得關(guān)聯(lián)一個(gè)真實(shí)對(duì)象,即被代理類實(shí)例。
接著,我們從外部調(diào)用代理類的任一方法,從反編譯的源碼我們知道,代理類方法會(huì)轉(zhuǎn)而去調(diào)用處理器的 invoke 方法并傳入方法簽名和方法的形式參數(shù)集合。
最后,方法能否得到正常的調(diào)用取決于處理器 invoke 方法體是否實(shí)實(shí)在在去調(diào)用了 method 方法。
其實(shí),基于 JDK 實(shí)現(xiàn)的的動(dòng)態(tài)代理是有缺陷的,并且這些缺陷是不易修復(fù)的,所以才有了 CGLIB 的流行。
一些缺陷與不足
單一的代理機(jī)制
不知道大家注意到我們上述的例子沒有,虛擬機(jī)生成的代理類為了公用 Proxy 類中的 InvocationHandler 字段來存儲(chǔ)自己的處理器類實(shí)例而繼承了 Proxy 類,那說明了什么?
Java 的單根繼承告訴你,代理類不能再繼承任何別的類了,那么被代理類父類中的方法自然就無從獲取,即代理類無法代理真實(shí)類中父類的任何方法。
除此之外的是另一個(gè)小細(xì)節(jié),不知道大家有沒有注意到,我特意這樣寫的。
這里的 sayHello 方法是實(shí)現(xiàn)的接口 IService,而 doService 方法則是獨(dú)屬于 realClass 自己的方法。但是我們從代理類中并沒有看到這個(gè)方法,也就是說這個(gè)方法沒有被代理。
所以說,JDK 的動(dòng)態(tài)代理機(jī)制是單一的,它只能代理被代理類的接口集合中的方法。
不友好的返回值
大家注意一下,newProxyInstance 返回的是代理類 「$Proxy0」 的一個(gè)實(shí)例,但是它是以 Object 類型進(jìn)行返回的,而你又不能強(qiáng)轉(zhuǎn)這個(gè) Object 實(shí)例到 「$Proxy0」 類型。
雖然我們知道這個(gè) Object 實(shí)例其實(shí)就是 「$Proxy0」 類型,但編譯期是不存在這個(gè) 「$Proxy0」 類型的,編譯器自然不會(huì)允許你強(qiáng)轉(zhuǎn)為一個(gè)不存在的類型了。所以一般只會(huì)強(qiáng)轉(zhuǎn)為該代理類實(shí)現(xiàn)的接口之一。
realClass rc = new realClass(); MyHanlder hanlder = new MyHanlder(rc); IService obj = (IService)Proxy.newProxyInstance( rc.getClass().getClassLoader(), new Class[]{IService.class}, hanlder); obj.sayHello();
程序運(yùn)行輸出:
proxy begainning..... hello world..... proxy ending.....
那么問題又來了,假如我們的被代理類實(shí)現(xiàn)了多個(gè)接口,請(qǐng)問你該強(qiáng)轉(zhuǎn)為那個(gè)接口類型,現(xiàn)在假設(shè)被代理類實(shí)現(xiàn)了接口 A 和 B,那么最后的實(shí)例如果強(qiáng)轉(zhuǎn)為 A ,自然被代理類所實(shí)現(xiàn)的接口 B 中所有的方法你都不能調(diào)用,反之亦然。
這樣就直接導(dǎo)致一個(gè)結(jié)果,你得清楚哪個(gè)方法是哪個(gè)接口中的,調(diào)用某個(gè)方法之前強(qiáng)轉(zhuǎn)為對(duì)應(yīng)的接口,相當(dāng)不友好的設(shè)計(jì)。
以上是我們認(rèn)為基于 JDK 的動(dòng)態(tài)代理機(jī)制所不太優(yōu)雅的設(shè)計(jì)之處,當(dāng)然了,它的優(yōu)點(diǎn)肯定是大于這些缺點(diǎn)的,下一篇我們將介紹一個(gè)廣為各類框架使用的 CGLIB 動(dòng)態(tài)代理庫,它的底層基于字節(jié)碼操作框架 ASM,不再依賴?yán)^承來實(shí)現(xiàn),完美的解決了 JDK 的單一代理的不足。
文章中的所有代碼、圖片、文件都云存儲(chǔ)在我的 GitHub 上:
(https://github.com/SingleYam/overview_java)
大家也可以選擇通過本地下載。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
java實(shí)現(xiàn)的順時(shí)針/逆時(shí)針打印矩陣操作示例
這篇文章主要介紹了java實(shí)現(xiàn)的順時(shí)針/逆時(shí)針打印矩陣操作,涉及java基于數(shù)組的矩陣存儲(chǔ)、遍歷、打印輸出等相關(guān)操作技巧,需要的朋友可以參考下2019-12-12seata-1.4.0安裝及在springcloud中使用詳解
這篇文章主要介紹了seata-1.4.0安裝及在springcloud中使用,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12java與scala數(shù)組及集合的基本操作對(duì)比
這篇文章主要介紹了java與scala數(shù)組及集合的基本操作對(duì)比,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10IDEA實(shí)現(xiàn)遠(yuǎn)程調(diào)試步驟詳解
這篇文章主要介紹了IDEA實(shí)現(xiàn)遠(yuǎn)程調(diào)試步驟詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09Spring boot通過AOP防止API重復(fù)請(qǐng)求代碼實(shí)例
這篇文章主要介紹了Spring boot通過AOP防止API重復(fù)請(qǐng)求代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12java實(shí)現(xiàn)求兩個(gè)字符串最長公共子串的方法
這篇文章主要介紹了java實(shí)現(xiàn)求兩個(gè)字符串最長公共子串的方法,是一道華為OJ上的一道題目,涉及Java針對(duì)字符串的遍歷、轉(zhuǎn)換及流程控制等技巧,需要的朋友可以參考下2015-12-12SpringBoot通過@MatrixVariable進(jìn)行傳參詳解
這篇文章主要介紹了SpringBoot使用@MatrixVariable傳參,文章圍繞@MatrixVariable展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-06-06