在解釋代理模式(靜態(tài)代理和動態(tài)代理)之前,先講一段故事:
小武跟大多數(shù)人一樣在廣州這座大城市工作陈辱,老家在外地,辛苦忙活一整年就期盼寒假那幾天可以回老家陪家人吃團年飯放沖天炮感受下家的溫暖。春運那幾天是人口大遷徙啊,北上廣深的大批人馬都順勢回流到內(nèi)陸地區(qū)...火車票什么的相應的就是一票難求美侦,使用一些官方購票APP根本搶不到車票好嘛,但小武跟其他很多人一樣需要回家啊魂奥。所以他必須需要尋求外力的幫助菠剩,這時朋友推薦了一個黃牛給他,黃牛說我也可以幫你買票耻煤,但是你需要提供乘車人基本信息以及票價和手續(xù)費然后我在去幫你搶具壮。小武說,沒問題啊只要能買到就行哈蝇。
我想棺妓,這是很多平凡人比較真實的寫照。
這段故事簡潔點描述就是自己想要在正規(guī)渠道買票炮赦,發(fā)現(xiàn)基本買不了怜跑。但是委托黃牛(代理)讓他帶我們?nèi)ベI卻有很大幾率買到車票。那么吠勘,這種通過增加一個中間件然后去進行實際操作的模式妆艘,我們一般稱之為代理模式。對于代理模式比較科學的定義是這樣:給某一個對象(目標對象)提供一個代理看幼,并由代理對象控制原對象(目標對象)的引用進行操作批旺,這種模式一般稱之為代理模式。
代理模式是結構型模式中的一種诵姜。既然要弄清楚代理模式汽煮,那么必須要先了解什么是結構性設計模式。結構性設計模式存在的目的主要是:在解決了對象的創(chuàng)建問題之后棚唆,對象的組成以及對象之間的依賴關系就成了開發(fā)人員關注的焦點暇赤。因為如何設計對象的結構、繼承和依賴關系會影響到后續(xù)程序的維護性宵凌、代碼的健壯性鞋囊、耦合性等等。因此瞎惫,結構性模式最主要涉及和關心的是如何組合類和對象來獲得更大的結構溜腐。結構性模式采用繼承機制來組合接口或實現(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)容需要翻墻才可以。如果要成功翻墻,大家能夠想到的是通過一些工具進行代理(你如果說我人直接在墻外這樣不就可以了嘛缺狠,要是這樣那就尷尬了)问慎,代理成功以后才可以進行查閱,那么這本質上也是一種代理模式挤茄。
總結:
代理模式主要針對的是直接訪問對象時可能會帶來的問題如叼,比如有些對象由于某些原因(例如對象創(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)代理操作,首先簡單瞄下這個方法的源碼:
其中紅色矩形的是方法的注釋,英文翻譯過來就是:返回指定接口的代理類的實例僵娃,該接口將方法調(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)代理模式(老司機可能在上面看到了method.invoke這個方法):
多提一嘴,可能各位看官更習慣這種寫法(也就是將InvocationHandler這個接口單獨拿出來寫)
然后開始編寫測試類:
這兩種動態(tài)代理寫法本質是一樣的浴讯,其實就是將實現(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-2 這幅圖中的藍色矩形狼忱,這個英文注釋翻譯過來就是: 查找或生成指定的代理類膨疏。也就是說getProxyClass0()這個方法生成的是具體的代理類;接著我們看下紅色矩形中的英文注釋钻弄,翻譯過來就是:用指定的調(diào)用處理程序調(diào)用它的構造函數(shù)佃却,簡單點說就是將InvocationHandler 這個對象傳入代理對象中。首先它通過類對象的getConstructor()方法獲得構造器對象窘俺,然后并調(diào)用其newInstance()方法創(chuàng)建對象饲帅。這個方法里面的其他代碼大概意思,復制代理類實現(xiàn)的所有接口 、獲取安全管理器 洒闸、進行一些權限檢驗等等染坯。但是,我們還是把注意力集中在getProxyClass0()這個方法丘逸,要分析它是如何生成的具體代理類单鹿?點進源碼看看:
首先,看下這個方法的英文注釋(也就是藍色矩形)翻譯過來就是:生成一個代理類深纲。必須調(diào)用checkProxyAccess方法仲锄,在調(diào)用這個方法之前需要檢查權限。下面的紅色箭頭是不是看到了在Android開發(fā)中熟悉的65535異常湃鹊?這里也做了相應的判斷儒喊;接著我們再看看紅色矩形的注釋,翻譯過來就是:如果由給定的裝載機定義的代理類實現(xiàn)給定的接口存在币呵,這將會返回緩存怀愧;否則,它將通過ProxyClassFactory創(chuàng)建代理類余赢。由于第一次運行加載是沒有緩存的芯义,所以我們需要進入ProxyClassFactory去了解這里如何創(chuàng)建代理類。
源碼很長妻柒,所以分了幾段代碼截圖(沒辦法誰叫我們是程序員扛拨,必要的耐心還是要有的)下面我們就逐個分析:
首先是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 源碼鏈接?這里給大家提供了外鏈方便查閱。
截圖中的第323行代碼伙狐,這里通過調(diào)用??generateClassFile()實例方法來生成Class文件涮毫。這個方法又做了什么,繼續(xù)跟進源碼(這里的源碼我單獨截圖出來贷屎,代碼里面已經(jīng)寫好相應的注釋罢防,這樣方便閱讀)
這里主要是寫入具體的內(nèi)容,部分源碼省略
總結:上面的代碼截圖主要是做了以下幾個工作:
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文件是這樣(我使用的是 jd_gui 來進行對class文件閱讀)下面省略部分源碼
從這個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:著作權歸作者所有,轉載請注明作者, 商業(yè)轉載請聯(lián)系作者獲得授權皆刺,非商業(yè)轉載請注明出處(開頭或結尾請?zhí)砑愚D載出處,添加原文url地址),文章請勿濫用,也希望大家尊重筆者的勞動成果凌摄。