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