『動態代理』其實源於設計模式中的代理模式,而代理模式就是使用代理對象完成用戶請求,屏蔽用戶對真實對象的訪問。
舉個最簡單的例子,比如我們想要「翻牆」訪問國外網站,因為我們並沒有牆掉所有國外的 IP,所以你可以將你的請求數據報發送到那些沒有被屏蔽的國外主機上,然后你通過配置國外主機將請求轉發到目的地並在得到響應報文后轉發回我們國內主機上。
這個例子中,國外主機就是一個代理對象,而那些被牆掉的主機就是真實對象,我們不能直接訪問到真實對象,但可以通過一個代理間接的訪問到。
代理模式的一個好處就是,所有的外部請求都經過代理對象,而代理對象有權利控制是否允許你真正的訪問到真實對象,如果不合法的請求,代理對象完全可以拒絕你而不用實際麻煩到真實對象。
代理模式的一個最典型的應用就是 Spring 框架,Spring 的 AOP 以面向切面式編程將實際的業務邏輯和相關日志異常等信息隔離開,而你每次對業務邏輯的請求都對應的是一個代理對象,這個代理對象中除了進行必要的權限檢查,日志打印,就是真實的業務邏輯處理塊。
靜態代理
代理模式的實現者主要有兩種,『靜態代理』和『動態代理』,這兩者的本質區別就在於,前者的代理類是需要程序員手動編碼的,而后者的代理類是自動生成的。所以,這也是你幾乎沒有聽過『靜態代理』這個概念的原因,當然,了解一下靜態代理自然更容易去理解『動態代理』。
有一點大家需要清楚,代理對象代理了真實對象所有的方法,也就是代理對象需要向外提供至少和真實對象一樣的方法名供調用,所以一個代理對象就需要定義出真實對象擁有的所有方法,包括父類中的方法。
我們看一個簡單的靜態代理示例:
為了說明問題,我們定義了一個 IService 接口,並讓我們的真實類繼承並實現該接口,這樣我們的真實類中就有兩個方法了。
那么代理類該怎樣定義才能完成對真實對象的代理呢?
一般來說,代理類的本質就是,定義出真實類中所有的方法並在方法內部添加一些其他操作,最后再調用真實類的該方法。
代理類要代理真實類中所有的方法,也就是說需要定義和真實類中那些方法簽名一模一樣的方法,而這些方法的內部還是會間接調用真實類的該方法。
所以一般來說,代理類會選擇直接繼承真實類所有的接口和父類以便拿到真實類所有的父級方法簽名,也就是先代理所有的父級方法。
接着,代理真實類中非父級方法,以這里的例子來說,doService 方法就是真實類自己的方法,我們的代理類也要定義一個一模一樣方法簽名的方法對其進行代理。
這樣,我們的代理類就算是完成了,以后對於真實類中所有方法的調用都可以通過代理類進行代理。像這樣:
public static void main(String[] args){
realClass realClass = new realClass();
ProxyClass proxyClass = new ProxyClass(realClass);
proxyClass.sayHello();
proxyClass.doService();
}
proxyClass 作為一個代理類對象,可以代理真實類中所有的方法,並在這些方法執行之前,打印了一些「無關緊要」的信息。
代理模式的一個基本實現思路基本是這樣,但是動態代理不同於這種靜態代理的一點在於,動態代理不用我們一個一個方法的定義,虛擬機會自動為你生成這些方法。
JDK 動態代理機制
動態代理區別於靜態代理的一點是,動態代理的代理類由虛擬機在運行時動態創建並於虛擬機卸載時清除。
我們復用上述靜態代理中使用的類,看看 JDK 的動態代理具體是如何做到代理出某個類實例的所有方法的。
定義一個 Handler 處理類:
Main 函數中調用 JDK 的動態代理 API 生成代理類實例:
涉及的代碼還是比較多的,我們一點點來分析。首先,realClass 作為我們的被代理類實現了接口 IService 並在內部定義了一個自己的方法 doService。
接着,我們定義了一個處理類,它繼承了接口 InvocationHandler 並實現了其唯一申明的 invoke 方法。除此之外,我們還得聲明一個成員字段用於存儲真實對象,也就是被代理對象,因為我們代理的任何方法基本上都是基於真實對象的相關方法的。
關於這個 invoke 方法的作用以及各個形式參數的意義,待會我們反射代理類源碼的時候再做詳細的分析。
最后,定義好我們的處理類,基本上就可以進行基於 JDK 的動態代理了。核心的方法是 Proxy 類的 newProxyInstance 方法,該方法有三個參數,其一是一個類加載器,其二是被代理類實現的所有接口集合,其三是我們自定義的處理器類。
虛擬機會在運行時使用你提供的類加載器,將所有指定的接口類加載進方法區,然后反射讀取這些接口中的方法並結合處理器類生成一個代理類型。
最后一句話可能有點抽象,如何「結合處理器類生成一個代理類型」?這一點我們通過指定虛擬機啟動參數,讓它保存下來生成的代理類的 Class 文件。
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
我們通過第三方工具反編譯這個 Class 文件,內容比較多,我們拆分了分析:
首先,這個代理類的名字是很隨意的,一個程序中如果有多個代理類要生成,「$Proxy + 數字」就是它們的類名。
接着,你會注意到這個代理類繼承 Proxy 類和我們指定的接口 IService(之前如果指定多個接口,這里就會繼承多個接口)。
然后你會發現,這個構造器需要一個 InvocationHandler 類型的參數,並且構造器的主體就是將這個 InvocationHandler 實例傳遞到父類 Proxy 的對應字段進行保存,這也是為什么所有的代理類都必須使用 Proxy 作為父類的一個原因,就是為了公用父類中的 InvocationHandler 字段。后面我們會知道,這一個小小的設計將導致基於 JDK 的動態代理存在一個致命性的缺點,待會介紹。
這一塊內容也算是代理類中較為重要的部分了,它將於虛擬機靜態初始化這個代理類的時候執行。這一大段代碼就是完成反射出所有接口中方法的功能,所有被反射出來的方法都對應一個 Method 類型的字段進行存儲。
除此之外,虛擬機還反射了 Object 中的三個常用方法,也就是說,代理類還會代理真實對象從 Object 那繼承來的這三個方法。
最后一部分我們看到的就是,虛擬機根據靜態初始化代碼塊反射出來所有待代理的方法,為它們生成代理的方法。
這些方法看起來好多代碼,其實就一行代碼,從父類 Proxy 中取出構造實例化時存入的處理器類,並調用它的 invoke 方法。
方法的參數基本一樣,第一個參數是當前代理類實例(事實證明這個參數傳過去並沒什么用),第二個參數是 Method 方法實例,第三個參數是方法的形式參數集合,如果沒有就是 null。
而這會兒我們再來看看當初自定的處理器類:
所有的代理類方法內部都會調用處理器類的 invoke 方法並傳入被代理類的當前方法,而這個 invoke 方法可以選擇去讓 method 正常被調用,也可以跳過 method 的調用,甚至可以在 method 真正被調用前后做一些額外的事情。
這,就是 JDK 動態代理的核心思想,我們稍微總結一下整個調用流程。
首先,一個處理器類的定義是必不可少的,它的內部必須得關聯一個真實對象,即被代理類實例。
接着,我們從外部調用代理類的任一方法,從反編譯的源碼我們知道,代理類方法會轉而去調用處理器的 invoke 方法並傳入方法簽名和方法的形式參數集合。
最后,方法能否得到正常的調用取決於處理器 invoke 方法體是否實實在在去調用了 method 方法。
其實,基於 JDK 實現的的動態代理是有缺陷的,並且這些缺陷是不易修復的,所以才有了 CGLIB 的流行。
一些缺陷與不足
單一的代理機制
不知道大家注意到我們上述的例子沒有,虛擬機生成的代理類為了公用 Proxy 類中的 InvocationHandler 字段來存儲自己的處理器類實例而繼承了 Proxy 類,那說明了什么?
Java 的單根繼承告訴你,代理類不能再繼承任何別的類了,那么被代理類父類中的方法自然就無從獲取,即代理類無法代理真實類中父類的任何方法。
除此之外的是另一個小細節,不知道大家有沒有注意到,我特意這樣寫的。
這里的 sayHello 方法是實現的接口 IService,而 doService 方法則是獨屬於 realClass 自己的方法。但是我們從代理類中並沒有看到這個方法,也就是說這個方法沒有被代理。
所以說,JDK 的動態代理機制是單一的,它只能代理被代理類的接口集合中的方法。
不友好的返回值
大家注意一下,newProxyInstance 返回的是代理類 「$Proxy0」 的一個實例,但是它是以 Object 類型進行返回的,而你又不能強轉這個 Object 實例到 「$Proxy0」 類型。
雖然我們知道這個 Object 實例其實就是 「$Proxy0」 類型,但編譯期是不存在這個 「$Proxy0」 類型的,編譯器自然不會允許你強轉為一個不存在的類型了。所以一般只會強轉為該代理類實現的接口之一。
realClass rc = new realClass();
MyHanlder hanlder = new MyHanlder(rc);
IService obj = (IService)Proxy.newProxyInstance(
rc.getClass().getClassLoader(),
new Class[]{IService.class},
hanlder);
obj.sayHello();
程序運行輸出:
proxy begainning.....
hello world.....
proxy ending.....
那么問題又來了,假如我們的被代理類實現了多個接口,請問你該強轉為那個接口類型,現在假設被代理類實現了接口 A 和 B,那么最后的實例如果強轉為 A ,自然被代理類所實現的接口 B 中所有的方法你都不能調用,反之亦然。
這樣就直接導致一個結果,你得清楚哪個方法是哪個接口中的,調用某個方法之前強轉為對應的接口,相當不友好的設計。
以上是我們認為基於 JDK 的動態代理機制所不太優雅的設計之處,當然了,它的優點肯定是大於這些缺點的,下一篇我們將介紹一個廣為各類框架使用的 CGLIB 動態代理庫,它的底層基於字節碼操作框架 ASM,不再依賴繼承來實現,完美的解決了 JDK 的單一代理的不足。
文章中的所有代碼、圖片、文件都雲存儲在我的 GitHub 上:
(https://github.com/SingleYam/overview_java)
歡迎關注微信公眾號:OneJavaCoder,所有文章都將同步在公眾號上。