設計模式之死磕代理模式(原創(chuàng))

在解釋代理模式(靜態(tài)代理和動態(tài)代理)之前,先講一段故事:

小武跟大多數(shù)人一樣在廣州這座大城市工作,老家在外地,辛苦忙活一整年就期盼寒假那幾天可以回老家陪家人吃團年飯放沖天炮感受下家的溫暖。春運那幾天是人口大遷徙啊,北上廣深的大批人馬都順勢回流到內(nèi)陸地區(qū)...火車票什么的相應的就是一票難求,使用一些官方購票APP根本搶不到車票好嘛,但小武跟其他很多人一樣需要回家啊。所以他必須需要尋求外力的幫助,這時朋友推薦了一個黃牛給他,黃牛說我也可以幫你買票,但是你需要提供乘車人基本信息以及票價和手續(xù)費然后我在去幫你搶。小武說,沒問題啊只要能買到就行。

我想,這是很多平凡人比較真實的寫照。

這段故事簡潔點描述就是自己想要在正規(guī)渠道買票,發(fā)現(xiàn)基本買不了。但是委托黃牛(代理)讓他帶我們?nèi)ベI卻有很大幾率買到車票。那么,這種通過增加一個中間件然后去進行實際操作的模式,我們一般稱之為代理模式。對于代理模式比較科學的定義是這樣:給某一個對象(目標對象)提供一個代理,并由代理對象控制原對象(目標對象)的引用進行操作,這種模式一般稱之為代理模式。

代理模式是結構型模式中的一種。既然要弄清楚代理模式,那么必須要先了解什么是結構性設計模式。結構性設計模式存在的目的主要是:在解決了對象的創(chuàng)建問題之后,對象的組成以及對象之間的依賴關系就成了開發(fā)人員關注的焦點。因為如何設計對象的結構、繼承和依賴關系會影響到后續(xù)程序的維護性、代碼的健壯性、耦合性等等。因此,結構性模式最主要涉及和關心的是如何組合類和對象來獲得更大的結構。結構性模式采用繼承機制來組合接口或?qū)崿F(xiàn)(也稱為:類結構性模式),或者通過組合一些對象從而實現(xiàn)新的功能(也稱為對象結構性模式)。綜上,結構性模式它不是對接口和實現(xiàn)進行組合,而是描述了如何對一些對象進行組合。

那么,如何通過代碼去描繪這種代理模式的行為?繼續(xù)回到上面的例子,通過這段故事仔細分析下可以有以下結論:

A:既然小武和黃牛都有買票的行為規(guī)范,那么可以定義一個接口讓他們都實現(xiàn)這個接口

B:雖然是黃牛在操作買票,但是買票的乘車人信息都是小武本人(畢竟實名制驗證),也就是說黃牛需要持有小武的個人信息才可以買票(用代碼的方式表達就是,黃牛這個類 需要持有小武這個對象的引用)

C:雖然進行買票操作的是黃牛,但實際的邏輯還是小武買票(小武可以在代售點、APP、火車站、黃牛進行具體的操作)

D:黃牛可以有多個,小武可以找多個黃牛;小武也可以有多種購票方式,除了尋求黃牛幫助、也可以去火車站、代售點、APP、12306官網(wǎng)、電話訂票進行訂票,所以,這種方式拓展性較強

理清上面幾個基本結論之后,我們首先定義一個買票接口(上面也說到了小武跟黃牛都需要遵循這個行為規(guī)范)

購票接口

接著,讓小武實現(xiàn)這個接口,目前,小武只是要買票(但是他要提供個人信息,個人信息可以提供給黃牛去操作也可以在12306上面進行購買,但是他必須要提供出來)于是可以有以下代碼:

小武

接著我們在定義黃牛,上面說了黃牛首先需要持有小武的對象引用;然后,黃牛買票其實就是小武在間接買票(所以實際操作的是還是小武買票的邏輯):

黃牛

那么,紅色矩形代表的就是小武的買票操作(因為黃牛只是代理)

以上代碼簡單定義完了小武和黃牛,既然黃牛這個對象我們已經(jīng)構建完畢了,我們只需要實例化一個對象,讓他幫我們進行購買即可,下面是測試代碼的編寫:

代理模式測試

通過以上代碼我們可以看到,我們調(diào)用黃牛的購票接口實際上是調(diào)用小武的功能邏輯。為了加深對代理模式的理解,這里在舉個例子:天朝的墻可謂是又高又厚,想要了解外面世界的一些內(nèi)容需要翻墻才可以。如果要成功翻墻,大家能夠想到的是通過一些工具進行代理(你如果說我人直接在墻外這樣不就可以了嘛,要是這樣那就尷尬了),代理成功以后才可以進行查閱,那么這本質(zhì)上也是一種代理模式。

總結:

代理模式主要針對的是直接訪問對象時可能會帶來的問題,比如有些對象由于某些原因(例如對象創(chuàng)建開銷很大,或者某些操作需要安全控制,或者需要進程外的訪問),直接訪問會給使用者或者系統(tǒng)結構帶來很多麻煩,因此我們可以在訪問此對象時,加上一個對此對象的訪問中間層。其優(yōu)點就在于拓展性極高(黃牛不僅可以賣火車票,它也可以推薦你買飛機票、買長途大巴票)、內(nèi)部功能隱蔽;缺點就是因為增加了中間件,這樣多余的一層可能會造成請求的處理速度相對變慢。

上面代碼描繪的代理模式,更加細分的話一般稱為靜態(tài)代理模式,為什么稱為靜態(tài)代理?因為代理對象需要與目標對象實現(xiàn)一樣的接口(也就是黃牛和小武實現(xiàn)購票接口),假設現(xiàn)在有這種情況,小武為了保守起見不僅找黃牛,自己還去代售點、去火車站、去各種APP上面進行買票,這種情況下就會有很多代理類(黃牛、代售點、車站、APP),代理類如果太多會出現(xiàn)什么樣的問題?一旦接口增加方法,目標對象與代理對象都要維護,這樣的話代碼改起來就相當煩瑣和冗余。為了解決這個問題,Java給我們提供了一種解決方式,這種解決方式大家一般稱為動態(tài)代理。

動態(tài)代理:

Java使用動態(tài)代理的關鍵是使用Proxy這個類以及InvocationHandler這個接口,調(diào)用Proxy這個類里面的newProxyInstance 方法進行具體的動態(tài)代理操作,首先簡單瞄下這個方法的源碼:

newProxyInstance?
英文注釋

其中紅色矩形的是方法的注釋,英文翻譯過來就是:返回指定接口的代理類的實例,該接口將方法調(diào)用分發(fā)給指定的調(diào)用處理程序。這個靜態(tài)方法有三個參數(shù):

參數(shù)一:ClassLoader loader,指定當前目標對象使用類加載器,我們知道獲取加載器的寫法是固定的

也就是,Object.getClass().getClassLoader()

參數(shù)二:Class<?>[ ] interfaces,目標對象實現(xiàn)的接口的類型,使用泛型方式確認類型,我們知道要想知道一個類是否實現(xiàn)某個接口,可以使用????Object.getClass().getInterfaces() ,這個方法是獲取類是否實現(xiàn)接口,如果此對象表示一個類,則返回值是一個數(shù)組,它包含了表示該類所實現(xiàn)的所有接口的對象。

參數(shù)三:InvocationHandler h,事件處理,執(zhí)行目標對象的方法時,會觸發(fā)事件處理器的方法,會把當前執(zhí)行目標對象的方法作為參數(shù)傳入

為了方便測試,我把動態(tài)代理的類單獨寫一個類這樣方便我們測試和對比,代理模式的寫法如下:

動態(tài)代理例子

寫完之后我們測試一下動態(tài)代理模式(老司機可能在上面看到了method.invoke這個方法):

動態(tài)代理測試

多提一嘴,可能各位看官更習慣這種寫法(也就是將InvocationHandler這個接口單獨拿出來寫)

實現(xiàn)InvocationHandler

然后開始編寫測試類:

測試動態(tài)代理

這兩種動態(tài)代理寫法本質(zhì)是一樣的,其實就是將實現(xiàn)了接口的代理類傳到?Proxy.newProxyInstance 中。基本的寫法就是這樣(后面有源碼分析)

動態(tài)代理模式和靜態(tài)代理模式這兩種模式有那些區(qū)別?我們首先通過對比兩種模式下的測試代碼來分析:

兩種模式對比

其中:藍色矩形代表的是動態(tài)代理模式,紅色矩形代表的是靜態(tài)代理模式?

通過這兩種模式可以很清晰看到 靜態(tài)代理模式指向的是實現(xiàn)了接口的具體代理類;而動態(tài)模式指向的是代理對象和目標對象使用的共同接口。很明顯,動態(tài)代理(接口)會比靜態(tài)代理模式(實現(xiàn)接口的子類)拓展性強

說完了動態(tài)模式的使用,下面開始深挖動態(tài)代理機制以及源碼:

細心的老司機會發(fā)現(xiàn),我們使用代理對象調(diào)用接口時候均調(diào)用了InvocationHandler這個接口的invoker方法,但內(nèi)部邏輯使用的是Method反射機制來執(zhí)行被代理對象(也就是目標對象)的接口方法。首先點進newProxyInstance這個方法一探究竟:

newProxyInstance - 1
newProxyInstance - 2

首先看newProxyInstance-2 這幅圖中的藍色矩形,這個英文注釋翻譯過來就是: 查找或生成指定的代理類。也就是說getProxyClass0()這個方法生成的是具體的代理類;接著我們看下紅色矩形中的英文注釋,翻譯過來就是:用指定的調(diào)用處理程序調(diào)用它的構造函數(shù),簡單點說就是將InvocationHandler 這個對象傳入代理對象中。首先它通過類對象的getConstructor()方法獲得構造器對象,然后并調(diào)用其newInstance()方法創(chuàng)建對象。這個方法里面的其他代碼大概意思,復制代理類實現(xiàn)的所有接口 、獲取安全管理器 、進行一些權限檢驗等等。但是,我們還是把注意力集中在getProxyClass0()這個方法,要分析它是如何生成的具體代理類?點進源碼看看:

getProxyClass0

首先,看下這個方法的英文注釋(也就是藍色矩形)翻譯過來就是:生成一個代理類。必須調(diào)用checkProxyAccess方法,在調(diào)用這個方法之前需要檢查權限。下面的紅色箭頭是不是看到了在Android開發(fā)中熟悉的65535異常?這里也做了相應的判斷;接著我們再看看紅色矩形的注釋,翻譯過來就是:如果由給定的裝載機定義的代理類實現(xiàn)給定的接口存在,這將會返回緩存;否則,它將通過ProxyClassFactory創(chuàng)建代理類。由于第一次運行加載是沒有緩存的,所以我們需要進入ProxyClassFactory去了解這里如何創(chuàng)建代理類。

ProxyClassFactory - 1
ProxyClassFactory ?- 1
ProxyClassFactory ?- 2
ProxyClassFactory - 3

源碼很長,所以分了幾段代碼截圖(沒辦法誰叫我們是程序員,必要的耐心還是要有的)下面我們就逐個分析:

首先是ProxyClassFactory - 1,首先看下這個方法的英文注釋,翻譯過來就是:這是一個工廠函數(shù)它主要生成、定義和返回給定的代理類加載器和接口數(shù)組;然后看下藍色矩形,這里面分別定義了代理類名稱前綴;用原子類來生成代理類的序號, 以此來確定唯一的代理類。接著,定義了一個Map,接著遍歷這個Map,遍歷Map的目的是判斷?intf 是否可以由指定的類加載進行加載、是否是一個接口、在數(shù)組中是否有重復。

然后是ProxyClassFactory - 2,這里面有代碼主要的意思是生成代理類的包名、生成代理類的訪問標志, 默認是public final(Modifier.PUBLIC | Modifier.FINAL)。藍色矩形內(nèi)的注釋翻譯過來就是:記錄一個非公共代理接口的包,以便代理類將在同一個包中定義。驗證所有非公有代理接口都在同一個包中。紅色矩形里面的意思主要有判斷權限、生成包名、截取包名、進行包名判斷(代理類如果實現(xiàn)不同包的接口, 并且接口都不是public的, 那么就會在這里拋異常)、代理類生成的包路徑位置,最后整合成一個代理類的最終的命名規(guī)則 = 包名 + 前綴 + 序號

最后是ProxyClassFactory - 3,黃色矩形就是上面說的命名規(guī)則(包名 + 前綴 + 序號),紅色矩形的英文注釋翻譯過來就是:生成指定的代理類。也就是說代理類的最后生成是在這里完成的。也就是:ProxyGenerator.generateProxyClass()。由于ProxyGenerator這個類在sun.misc這個包下,這個包下的代碼直接訪問不了,ProxyGenerator 源碼鏈接?這里給大家提供了外鏈方便查閱。

ProxyGenerator.generateProxyClass

截圖中的第323行代碼,這里通過調(diào)用??generateClassFile()實例方法來生成Class文件。這個方法又做了什么,繼續(xù)跟進源碼(這里的源碼我單獨截圖出來,代碼里面已經(jīng)寫好相應的注釋,這樣方便閱讀)

源碼 - 注釋1
源碼 - 注釋2
源碼 - 注釋3

這里主要是寫入具體的內(nèi)容,部分源碼省略

源碼 - 注釋4

總結:上面的代碼截圖主要是做了以下幾個工作:

A:收集要生成的代理方法,將其包裝成ProxyMethod對象并注冊到集合中。

B:收集所有要為Class文件生成的字段信息和方法信息。

C:將B步驟的字段和方法組裝成Class文件。

這里多提一嘴,我們平時編寫的Java文件是以 .java 結尾的,在編寫好了之后通過編譯器進行編譯會先生成.class文件(通過命令行可以生成class文件,命令符是: javac ?java文件)然后在運行。實際上Java程序的執(zhí)行只依賴于Class文件。這個Class文件描述了很多信息,當我們需要使用到一個類時,Java虛擬機就會提前去加載這個類的Class文件并進行初始化和相關的檢驗工作,Java虛擬機能夠保證在你使用到這個類之前就會完成這些工作。

那么,我們?nèi)绾芜€原動態(tài)代理生成的class文件,打破這最后一道壁壘?我們可以在上面測試動態(tài)代理的代碼中加入下面這一段(紅色區(qū)域):

還原class文件

最后生成的class文件是這樣(我使用的是 jd_gui 來進行對class文件閱讀)下面省略部分源碼

class文件 - 1
class 文件 -2?

從這個class文件 - 1截圖我們可以看到,由于Java架構起初的單繼承設計機制,生成的代理類默認是繼承Porxy這個類,因為單繼承的特性,所以JDK動態(tài)代理只能去實現(xiàn)接口。class文件 - 2截圖就很清楚了,這里的invoke中的m3,是通過反射調(diào)用接口中的方法(兩個紅色矩形),所以就解釋了動態(tài)代理為什么會要重寫InvocationHandler接口中的invoke方法。

動態(tài)代理模式源碼總結:

1:動態(tài)代理主要的方法是Proxy.newProxyInstance,生成動態(tài)代理的類調(diào)用的是getProxyClass0這個方法,實際是在ProxyClassFactory這里進行操作

2:ProxyClassFactory這個類里面主要進行權限判斷,包名的拼裝等一些操作最后通過ProxyGenerator.generateProxyClass這個方法生成二進制文件

3:ProxyGenerator這個類是最底層的核心,它收集所有要生成的代理方法,將其包裝成ProxyMethod對象并注冊到集合、收集字段信息和方法信息將其拼裝成class文件

4:通過查看生成的class文件,內(nèi)部使用的是反射,重點是invoke方法。

如果這篇文章對您有開發(fā)or學習上的些許幫助,希望各位看官留下寶貴的star,謝謝。

Ps:著作權歸作者所有,轉(zhuǎn)載請注明作者, 商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權,非商業(yè)轉(zhuǎn)載請注明出處(開頭或結尾請?zhí)砑愚D(zhuǎn)載出處,添加原文url地址),文章請勿濫用,也希望大家尊重筆者的勞動成果。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容