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