java 動態代理

轉載自知乎“The hope”的回答。如涉及到侵權,請聯繫博主。
=============================================
本文是我自己寫的博客,結合了我自己的一些思考,用來梳理 java 中的代理模式。應該也能解答題主的疑問,所以就貼過來了。
先來畫一張思維導圖圖總結一下:


以下是原文
---------------------我是分割線-------------------
代理,或者稱爲 Proxy ,簡單理解就是事情我不用去做,由其他人來替我完成。在黃勇《架構探險》一書中,我覺得很有意思的一句相關介紹是這麼說的:
賺錢方面,我就是我老婆的代理;帶小孩方面,我老婆就是我的代理;家務事方面,沒有代理。

我是一個很喜歡偷懶的程序猿,一看代理的定義,哇塞,還有這麼好的事情?居然可以委託別人替我幹活! 那麼倒底是不是這樣呢?彆着急,仔細看看本文關於代理技術的介紹,最後我會專門回過頭來解釋這個問題的。
本文主要介紹了無代理、靜態代理、JDK 動態代理、CGLib 動態代理的實現原理及其使用場景,及筆者對其使用邏輯的一點思考。限於本人的筆力和技術水平,難免有些說明不清楚的地方,權當拋磚引玉,還望海涵。
無代理

讓我們先看一個小栗子:

public interface Humen{ void eat(String food);}
上面是一個接口,下面是其實現類:

publicclassHumenImplimplementsHumen{@Overridepublicvoideat(Stringfood){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");}
打印姐結果如下:

cookeat riceswap
可以看到,我們使用 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") public T 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 動態代理

CGLib 是一個類庫,它可以在運行期間動態的生成字節碼,動態生成代理類。繼續上代碼:

public class CGLibProxy implements MethodInterceptor{ public T 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代理技術簡介
聯繫作者

發佈了41 篇原創文章 · 獲贊 23 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章