代理技術簡介

代理,或者稱爲 Proxy ,簡單理解就是事情我不用去作,由其餘人來替我完成。在黃勇《架構探險》一書中,我以爲頗有意思的一句相關介紹是這麼說的:java

賺錢方面,我就是我老婆的代理;帶小孩方面,我老婆就是個人代理;家務事方面,沒有代理。segmentfault

我是一個很喜歡偷懶的程序猿,一看代理的定義,哇塞,還有這麼好的事情?竟然能夠委託別人替我幹活! 那麼倒底是否是這樣呢?彆着急,仔細看看本文關於代理技術的介紹,最後我會專門回過頭來解釋這個問題的。架構

本文主要介紹了無代理、靜態代理、JDK 動態代理、CGLib 動態代理的實現原理及其使用場景,及筆者對其使用邏輯的一點思考。限於本人的筆力和技術水平,不免有些說明不清楚的地方,權當拋磚引玉,還望海涵。框架

無代理

讓咱們先看一個小栗子:ide

public interface Humen{

  void eat(String food);
}

上面是一個接口,下面是其實現類:測試

public class HumenImpl implements Humen{

  @Override
  public void eat(String food){
    System.out.println("eat " + food);
  }
}

拓展思考

在這裏咱們能夠稍微作些擴展思考。若是將來,咱們須要在這個 eat() 方法先後加上一些邏輯呢?好比說真實點的吃飯場景,第一步固然是要作飯,當咱們吃完之後,則須要有人打掃。this

固然,咱們能夠把作飯和打掃的邏輯一併寫在 eat() 方法內部,只是這樣作,顯然犧牲了不少的靈活性和拓展性。好比說,若是咱們今天決定不在家作飯了,咱們改去下館子,那麼這時候,顯然,我須要改變以前的作飯邏輯爲下館子。常規的做法是怎麼辦呢?有兩種:spa

  • 我再寫個eat()方法,兩個方法的名字/參數不一樣,在調用的時候多作注意,調用不一樣的方法/參數以實現執行不一樣的邏輯.net

  • 我再也不多寫個新方法,我在原來的方法中多傳個標誌位,在方法運行中經過if-else語句判斷這個標誌位,而後執行不一樣的邏輯3d

這兩種方法其實大同小異,本質上都是編譯時就設定死了使用邏輯,一個須要在調用階段多加判斷,另外一個在方法內部多作判斷。可是於業務場景拓展和代碼複用的角度來看,均是問題多多。

  • 假設我將來不下館子,也不本身作飯了,我蹭飯吃。這時候我就不須要作飯或者下訂單了,那麼按照上述處理思路,我至少要在全部調用的部分加個新標誌位,在處理邏輯中多加一重判斷,甚至或許多出了一個新方法。

  • 吃過飯須要進行打掃,我不當心弄灑了可樂也須要打掃,當我須要在別處調用打掃邏輯時,難以作到複用。

小結

聰明的客官確定想到了,既然把它們寫在一個方法中有這麼多問題,那麼咱們把邏輯拆開,吃飯就是吃飯,作飯就是作飯,打掃就是打掃不就行了嗎?事實確實是這樣沒錯。只是原有的老代碼人家就調用的是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")
  public <T> 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> T getProxy(Class<T> 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動態代理雖然不須要接口信息,可是它攔截幷包裝被代理類的全部方法。

最後,咱們畫一張思惟導圖總結一下:

clipboard.png

代理技術在實際項目中有很是多的應用,好比Spring 的AOP技術。下篇博客中,我將會着重介紹代理技術在 Spring 的AOP技術中是如何使用的相關思考,敬請期待~

參考文檔

  • 黃勇—《架構探險-從零開始寫Java Web框架》4.1代理技術簡介

聯繫做者

zhihu.com
segmentfault.com
oschina.net

相關文章
相關標籤/搜索