摘要:我是一個(gè)很喜歡偷懶的程序猿,一看代理的定義,哇塞,還有這么好的事情居然可以委托別人替我干活那么倒底是不是這樣呢別著急,仔細(xì)看看本文關(guān)于代理技術(shù)的介紹,最后我會(huì)專門回過頭來解釋這個(gè)問題的。
代理,或者稱為 Proxy ,簡單理解就是事情我不用去做,由其他人來替我完成。在黃勇《架構(gòu)探險(xiǎn)》一書中,我覺得很有意思的一句相關(guān)介紹是這么說的:
賺錢方面,我就是我老婆的代理;帶小孩方面,我老婆就是我的代理;家務(wù)事方面,沒有代理。
我是一個(gè)很喜歡偷懶的程序猿,一看代理的定義,哇塞,還有這么好的事情?居然可以委托別人替我干活! 那么倒底是不是這樣呢?別著急,仔細(xì)看看本文關(guān)于代理技術(shù)的介紹,最后我會(huì)專門回過頭來解釋這個(gè)問題的。
本文主要介紹了無代理、靜態(tài)代理、JDK 動(dòng)態(tài)代理、CGLib 動(dòng)態(tài)代理的實(shí)現(xiàn)原理及其使用場景,及筆者對(duì)其使用邏輯的一點(diǎn)思考。限于本人的筆力和技術(shù)水平,難免有些說明不清楚的地方,權(quán)當(dāng)拋磚引玉,還望海涵。
無代理讓我們先看一個(gè)小栗子:
public interface Humen{ void eat(String food); }
上面是一個(gè)接口,下面是其實(shí)現(xiàn)類:
public class HumenImpl implements Humen{ @Override public void eat(String food){ System.out.println("eat " + food); } }拓展思考
在這里我們可以稍微做些擴(kuò)展思考。如果未來,我們需要在這個(gè) eat() 方法前后加上一些邏輯呢?比如說真實(shí)點(diǎn)的吃飯場景,第一步當(dāng)然是要做飯,當(dāng)我們吃完以后,則需要有人打掃。
當(dāng)然,我們可以把做飯和打掃的邏輯一并寫在 eat() 方法內(nèi)部,只是這樣做,顯然犧牲了很多的靈活性和拓展性。比如說,如果我們今天決定不在家做飯了,我們改去下館子,那么這時(shí)候,顯然,我需要改變之前的做飯邏輯為下館子。常規(guī)的作法是怎么辦呢?有兩種:
我再寫個(gè)eat()方法,兩個(gè)方法的名字/參數(shù)不同,在調(diào)用的時(shí)候多做注意,調(diào)用不同的方法/參數(shù)以實(shí)現(xiàn)執(zhí)行不同的邏輯
我不再多寫個(gè)新方法,我在原來的方法中多傳個(gè)標(biāo)志位,在方法運(yùn)行中通過if-else語句判斷這個(gè)標(biāo)志位,然后執(zhí)行不同的邏輯
這兩種方法其實(shí)大同小異,本質(zhì)上都是編譯時(shí)就設(shè)定死了使用邏輯,一個(gè)需要在調(diào)用階段多加判斷,另一個(gè)在方法內(nèi)部多做判斷。但是于業(yè)務(wù)場景拓展和代碼復(fù)用的角度來看,均是問題多多。
假設(shè)我未來不下館子,也不自己做飯了,我蹭飯吃。這時(shí)候我就不需要做飯或者下訂單了,那么按照上述處理思路,我至少要在所有調(diào)用的部分加個(gè)新標(biāo)志位,在處理邏輯中多加一重判斷,甚至或許多出了一個(gè)新方法。
吃過飯需要進(jìn)行打掃,我不小心弄灑了可樂也需要打掃,當(dāng)我需要在別處調(diào)用打掃邏輯時(shí),難以做到復(fù)用。
小結(jié)聰明的客官肯定想到了,既然把它們寫在一個(gè)方法中有這么多問題,那么我們把邏輯拆開,吃飯就是吃飯,做飯就是做飯,打掃就是打掃不就好了嗎?事實(shí)確實(shí)是這樣沒錯(cuò)。只是原有的老代碼人家就調(diào)用的是eat()方法,那我們?nèi)绾螌?shí)現(xiàn)改動(dòng)最少的代碼又實(shí)現(xiàn)既做飯,又吃飯,然后還自帶打掃的全方位一體化功能呢?
靜態(tài)代理下面我們就用靜態(tài)代理模式改造下之前的代碼,看看是不是滿足了我們的需求。話不多說,上代碼~
public class HumenProxy implements Humen{ private Humen humen; public HumenProxy(){ humen = new HumenImpl(); } @Override public void eat(String food){ before(); humen.eat(food); after(); } private void before(){ System.out.println("cook"); } private void after(){ System.out.println("swap"); } }
用main方法測試一下:
public static void main(String[] args){ Humen humenProxy = new HumenProxy(); humenProxy.eat("rice"); }
打印姐結(jié)果如下:
cook eat rice swap
可以看到,我們使用 HumenProxy 實(shí)現(xiàn)了 Humen 接口(和 HumenImpl 實(shí)現(xiàn)相同接口),并在構(gòu)造方法中 new 出一個(gè) HumenImpl 類的實(shí)例。這樣一來,我們就可以在 HumenProxy 的 eat() 方法里面去調(diào)用 HumenImpl 方法的 eat() 方法了。有意思的是,我們?cè)谡{(diào)用邏輯部分( main() 方法),依然持有的是 Humen 接口類型的引用,調(diào)用的也依然是 eat() 方法,只是實(shí)例化對(duì)象的過程改變了,結(jié)果來看,代理類卻自動(dòng)為我們加上了 cook 和 swap 等我們需要的動(dòng)作。
小結(jié)小結(jié)一下,靜態(tài)代理,為我們帶來了一定的靈活性,是我們?cè)诓桓淖冊(cè)瓉淼谋淮眍惖姆椒ǖ那闆r下,通過在調(diào)用處替換被代理類的實(shí)例化語句為代理類的實(shí)例化語句的方式,實(shí)現(xiàn)了改動(dòng)少量的代碼(只改動(dòng)了調(diào)用處的一行代碼),就獲得額外動(dòng)作的功能。
拓展思考 優(yōu)點(diǎn)回看我們?cè)跓o代理方式實(shí)現(xiàn)中提出的兩個(gè)問題:
假設(shè)我未來不下館子,也不自己做飯了,我蹭飯吃。這時(shí)候我就不需要做飯或者下訂單了,那么按照上述處理思路,我至少要在所有調(diào)用的部分加個(gè)新標(biāo)志位,在處理邏輯中多加一重判斷,甚至或許多出了一個(gè)新方法。
吃過飯需要進(jìn)行打掃,我不小心弄灑了可樂也需要打掃,當(dāng)我需要在別處調(diào)用打掃邏輯時(shí),難以做到復(fù)用。
第一個(gè)問題,如果我們需要改變吃飯前后的邏輯怎么辦呢?現(xiàn)在不需要改變 HumenImpl 的 eat() 方法了,我們只需要在 HumenProxy 的 eat() 方法中改變一下調(diào)用邏輯就好了。當(dāng)然,如果需要同時(shí)保留原有的做飯和下訂單的邏輯的話,依然需要在 HumenProxy 添加額外的判斷邏輯或者直接寫個(gè)新的代理類,在調(diào)用處(本例中為 main() 方法)修改實(shí)例化的過程。
第二個(gè)問題,在不同的地方需要復(fù)用我的 cook() 或者 swap() 方法時(shí),我可以讓我的 HumenProxy 再實(shí)現(xiàn)別的接口,然后和這里的 eat() 邏輯一樣,讓業(yè)務(wù)代碼調(diào)用我的代理類即可。
缺點(diǎn)其實(shí)這里的缺點(diǎn)就是上述優(yōu)點(diǎn)的第二點(diǎn),當(dāng)我需要復(fù)用我的做飯邏輯時(shí),我的代理總是需要實(shí)現(xiàn)一個(gè)新的接口,然后再寫一個(gè)該接口的實(shí)現(xiàn)方法。但其實(shí)代理類的調(diào)用邏輯總是相似的,為了這么一個(gè)相似的實(shí)現(xiàn)效果,我卻總是要寫辣莫多包裝代碼,難道不會(huì)很累嗎?
另一方面,當(dāng)我們的接口改變的時(shí)候,無疑,被代理的類需要改變,同時(shí)我們的額代理類也需要跟著改變,難道沒有更好的辦法了么?
作為一個(gè)愛偷懶的程序猿,當(dāng)然會(huì)有相應(yīng)的解決辦法了~ 讓我們接下來看看JDK動(dòng)態(tài)代理。
JDK 動(dòng)態(tài)代理依然是先看看代碼:
public class DynamicProxy implements InvocationHandler{ private Object target; public DynamicProxy(Object target){ this.target = target; } @Override public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{ before(); Object result = method.invoke(traget,args); after(); return result; } }
在上述代碼中,我們一方面將原本代理類中的代理對(duì)象的引用類型由具體類型改為 Object 基類型,另一方面將方法的調(diào)用過程改為通過反射的方式,實(shí)現(xiàn)了不依賴于實(shí)現(xiàn)具體接口的具體方法,便成功代理被代理對(duì)象的方法的效果。
我們來繼續(xù)看看怎么調(diào)用:
public static void main(String[] args){ Humen humen = new HumenImpl(); DynamicProxy dynamicProxy = new DynamicProxy(humen); Humen HumenProxy = (Humen) Proxy.newProInstance( humen.getClass().getClassLoader(), humen.getClass().getInterfaces(), dynamicProxy ); humenProxy.eat("rice"); }
我們可以看到,在調(diào)用過程中,我們使用了通用的 DynamicProxy 類包裝了 HumenImpl 實(shí)例,然后調(diào)用了Jdk的代理工廠方法實(shí)例化了一個(gè)具體的代理類。最后調(diào)用代理的 eat() 方法。
我們可以看到,這個(gè)調(diào)用雖然足夠靈活,可以動(dòng)態(tài)生成一個(gè)具體的代理類,而不用自己顯示的創(chuàng)建一個(gè)實(shí)現(xiàn)具體接口的代理類,不過調(diào)用這個(gè)代理類的過程還是有些略顯復(fù)雜,與我們減少包裝代碼的目標(biāo)不符,所以可以考慮做些小重構(gòu)來簡化調(diào)用過程:
public class DynamicProxy implements InvocationHandler{ ··· @SuppressWarnings("unchecked") publicT getProxy(){ return (T) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), this ); } }
我們繼續(xù)看看現(xiàn)在的調(diào)用邏輯:
public static void main(String[] args){ DynamicProxy dynamicProxy = new DynamicProxy(new HumenImpl); Humen HumenProxy = dynamicProxy.getProxy(); humenProxy.eat("rice"); }拓展思考 優(yōu)點(diǎn)
相比之前的靜態(tài)代理,我們可以發(fā)現(xiàn),現(xiàn)在的調(diào)用代碼多了一行。不過相較這多出來的一行,更令人興奮的時(shí),我們通過實(shí)用 jdk 為我們提供的動(dòng)態(tài)代理實(shí)現(xiàn),達(dá)到了我們的 cook() 或者 swap() 方法可以被任意的復(fù)用的效果(只要我們?cè)谡{(diào)用代碼處使用這個(gè)通用代理類去包裝任意想要需要包裝的被代理類即可)。
當(dāng)接口改變的時(shí)候,雖然被代理類需要改變,但是我們的代理類卻不用改變了。
我們可以看到,無論是靜態(tài)代理還是動(dòng)態(tài)代理,它都需要一個(gè)接口。那如果我們想要包裝的方法,它就沒有實(shí)現(xiàn)接口怎么辦呢?這個(gè)問題問的好,JDK為我們提供的代理實(shí)現(xiàn)方案確實(shí)沒法解決這個(gè)問題。。。
那么怎么辦呢?別急,接下來就是我們的終極大殺器,CGLib動(dòng)態(tài)代理登場的時(shí)候了。
CGLib 是一個(gè)類庫,它可以在運(yùn)行期間動(dòng)態(tài)的生成字節(jié)碼,動(dòng)態(tài)生成代理類。繼續(xù)上代碼:
public class CGLibProxy implements MethodInterceptor{ publicT getProxy(Class cls){ return (T) Enhancer.create(cls,this); } public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy) throws Throwable{ before(); Object result = proxy.invokeSuper(obj,args); after(); return result; } }
調(diào)用時(shí)邏輯如下:
public static void main(String[] args){ CGLibProxy cgLibProxy = new CGLibProxy(); Humen humenProxy = cgLibProxy.getProxy(HumenImpl.class); humenProxy.eat("rice"); }
因?yàn)槲覀兊?CGLib 代理并不需要?jiǎng)討B(tài)綁定接口信息(JDK默認(rèn)代理需要用構(gòu)造方法動(dòng)態(tài)獲取具體的接口信息)。
所以其實(shí)這里調(diào)用 CGLib 代理的過程還可以再進(jìn)行簡化,我們只要將代理類定義為單例模式,即可使調(diào)用邏輯簡化為兩行操作:
public class CGLibproxy implements MethodInterceptor{ private static CGLibProxy instance = new CGLibProxy(); private CGLibProxy(){} public static CGLibProxy getInstance(){ return instance; } }
調(diào)用邏輯:
public static voidf main(String[] atgs){ Humen humenProxy = CGLibProxy.getInstance().getProxy(HumenImpl.class); humenProxy.eat("rice"); }拓展思考 優(yōu)點(diǎn)
實(shí)用 CGLib 動(dòng)態(tài)代理的優(yōu)勢很明顯,有了它,我們就可以為沒有接口的類包裝前置和后置方法了。從這點(diǎn)來說,它比無論是 JDK 動(dòng)態(tài)代理還是靜態(tài)代理都靈活的多。
缺點(diǎn)既然它比 JDK 動(dòng)態(tài)代理還要靈活,那么我為什么還要在前面花那么多篇幅去介紹 JDK 動(dòng)態(tài)代理呢?這就不得不提它的一個(gè)很大的缺點(diǎn)了。
我們想想,JDK 動(dòng)態(tài)代理 和它在調(diào)用階段有什么不同?對(duì),少了接口信息。那么JDK動(dòng)態(tài)代理為什么需要接口信息呢?就是因?yàn)橐鶕?jù)接口信息來攔截特定的方法,而CGLib動(dòng)態(tài)代理并沒接收接口信息,那么它又是如何攔截指定的方法呢?答案是沒有做攔截。。。(各位讀者可以自己試試)
總結(jié)通過上述介紹我們可以看到,代理是一種非常有意思的模式。本文具體介紹了三種代理實(shí)現(xiàn)方式,靜態(tài)代理、JDK動(dòng)態(tài)代理 以及 CGLib動(dòng)態(tài)代理。
這三種代理方式各有優(yōu)劣,它們的優(yōu)點(diǎn)在于:
我們通過在原有的調(diào)用邏輯過程中,再抽一個(gè)代理類的方式,使調(diào)用邏輯的變化盡可能的封裝再代理類的內(nèi)部中,達(dá)到不去改動(dòng)原有被代理類的方法的情況下,增加新的動(dòng)作的效果。
這就使得即便在未來的使用場景中有更多的拓展,改變也依然很難波及被代理類,我們也就可以放心的對(duì)被代理類的特定方法進(jìn)行復(fù)用了
從缺點(diǎn)來看:
靜態(tài)代理和JDK動(dòng)態(tài)代理都需要被代理類的接口信息以確定特定的方法進(jìn)行攔截和包裝。
CGLib動(dòng)態(tài)代理雖然不需要接口信息,但是它攔截并包裝被代理類的所有方法。
最后,我們畫一張思維導(dǎo)圖總結(jié)一下:
代理技術(shù)在實(shí)際項(xiàng)目中有非常多的應(yīng)用,比如Spring 的AOP技術(shù)。下篇博客中,我將會(huì)著重介紹代理技術(shù)在 Spring 的AOP技術(shù)中是如何使用的相關(guān)思考,敬請(qǐng)期待~
參考文檔黃勇—《架構(gòu)探險(xiǎn)-從零開始寫Java Web框架》4.1代理技術(shù)簡介
聯(lián)系作者zhihu.com
segmentfault.com
oschina.net
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/66776.html
閱讀 2570·2023-04-26 01:44
閱讀 2584·2021-09-10 10:50
閱讀 1423·2019-08-30 15:56
閱讀 2292·2019-08-30 15:44
閱讀 529·2019-08-29 11:14
閱讀 3431·2019-08-26 11:56
閱讀 3029·2019-08-26 11:52
閱讀 924·2019-08-26 10:27