溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

JDK動(dòng)態(tài)代理機(jī)制的示例分析

發(fā)布時(shí)間:2021-08-21 14:13:57 來源:億速云 閱讀:108 作者:小新 欄目:編程語(yǔ)言

這篇文章主要介紹了JDK動(dòng)態(tài)代理機(jī)制的示例分析,具有一定借鑒價(jià)值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

前言

『動(dòng)態(tài)代理』其實(shí)源于設(shè)計(jì)模式中的代理模式,而代理模式就是使用代理對(duì)象完成用戶請(qǐng)求,屏蔽用戶對(duì)真實(shí)對(duì)象的訪問。

舉個(gè)最簡(jiǎn)單的例子,比如我們想要「FQ」訪問國(guó)外網(wǎng)站,因?yàn)槲覀儾]有墻掉所有國(guó)外的 IP,所以你可以將你的請(qǐng)求數(shù)據(jù)報(bào)發(fā)送到那些沒有被屏蔽的國(guó)外主機(jī)上,然后你通過配置國(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ì)象,我們不能直接訪問到真實(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è)簡(jiǎn)單的靜態(tài)代理示例:

JDK動(dòng)態(tài)代理機(jī)制的示例分析

JDK動(dòng)態(tài)代理機(jī)制的示例分析

為了說明問題,我們定義了一個(gè) IService 接口,并讓我們的真實(shí)類繼承并實(shí)現(xiàn)該接口,這樣我們的真實(shí)類中就有兩個(gè)方法了。

那么代理類該怎樣定義才能完成對(duì)真實(shí)對(duì)象的代理呢?

JDK動(dòng)態(tài)代理機(jī)制的示例分析

一般來說,代理類的本質(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í)例的所有方法的。

JDK動(dòng)態(tài)代理機(jī)制的示例分析

JDK動(dòng)態(tài)代理機(jī)制的示例分析

定義一個(gè) Handler 處理類:

JDK動(dòng)態(tài)代理機(jī)制的示例分析

Main 函數(shù)中調(diào)用 JDK 的動(dòng)態(tài)代理 API 生成代理類實(shí)例:

JDK動(dòng)態(tài)代理機(jī)制的示例分析

涉及的代碼還是比較多的,我們一點(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)容比較多,我們拆分了分析:

JDK動(dòng)態(tài)代理機(jī)制的示例分析

首先,這個(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ì)介紹。

JDK動(dòng)態(tài)代理機(jī)制的示例分析

這一塊內(nèi)容也算是代理類中較為重要的部分了,它將于虛擬機(jī)靜態(tài)初始化這個(gè)代理類的時(shí)候執(zhí)行。這一大段代碼就是完成反射出所有接口中方法的功能,所有被反射出來的方法都對(duì)應(yīng)一個(gè) Method 類型的字段進(jìn)行存儲(chǔ)。

除此之外,虛擬機(jī)還反射了 Object 中的三個(gè)常用方法,也就是說,代理類還會(huì)代理真實(shí)對(duì)象從 Object 那繼承來的這三個(gè)方法。

JDK動(dòng)態(tài)代理機(jī)制的示例分析

最后一部分我們看到的就是,虛擬機(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)初自定的處理器類:

JDK動(dòng)態(tài)代理機(jī)制的示例分析

所有的代理類方法內(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é),不知道大家有沒有注意到,我特意這樣寫的。

JDK動(dòng)態(tài)代理機(jī)制的示例分析

這里的 sayHello 方法是實(shí)現(xiàn)的接口 IService,而 doService 方法則是獨(dú)屬于 realClass 自己的方法。但是我們從代理類中并沒有看到這個(gè)方法,也就是說這個(gè)方法沒有被代理。

所以說,JDK 的動(dòng)態(tài)代理機(jī)制是單一的,它只能代理被代理類的接口集合中的方法。

不友好的返回值

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ī)制的示例分析”這篇文章對(duì)大家有幫助,同時(shí)也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識(shí)等著你來學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

jdk
AI