java 代理模式-靜態代理與動態代理

  • 2019 年 10 月 3 日
  • 筆記

最近在研究SpringAOP,當然要學習AOP就要知道這麼健碩、強大的功能的背後究竟隱藏著怎樣不可告人的“秘密??

接下來就是查閱了許多資料詳細的研究了一下Java的代理模式,感覺還是非常非常重要的,

我們作為一個有“內涵的”程式設計師就更應該掌握啦!(本文需要細心、帶有審視的目光來甄別其中的內容)


在學習代理模式的時候我首先要提出幾個問題,

  1、什麼是代理模式?

    舉個例子吧:我們生活中的租房問題。假如我們去租個房子,我們大多數情況下是不會知道房主(就是真正租房,一手貨源)的,我們是不是都是先去某些租房平台,或者去找當地的中介去詢問何時的房子。我們通過九牛二虎之力在中介那裡找到了個物美價廉的房子後,你的租金是不是交給了中介,中介還會收取一些額外的推薦費啦,押金啦、手續費等之類的,那麼好,這樣的一小段,就已經出來了其中兩大核心對象了。

    房主(把房子交給中介的人):被代理對象

    中介(租給你房子的人):代理對象

  2、代理模式有哪些作用? 

    1.可以隱藏目標的的具體實現(還是拿上面租房的例子來說,房主把房子交給了中介,並和中介談好了價格我 7你3啊。然後當我們去租房子的時候,是中介正面把房子租給了我們,而真正背後賣房子的並未出面,這樣就隱藏了背後人的資訊和提高了背後人的安全)

    2.可以在不修改目標類程式碼的情況下,對其增加新的功能。(上面例子來說:房東把房子交給中介的時候價格可能只有1000,但是房東可以賣到5000,然後賣出去後在把1000給房東,自己收入4000,這樣原房東不但收到了應由的錢,中介還收入了更多的額外費用)。

  3、代理模式有哪幾種?分別都有什麼不同?

    在我們Java程式中代理模式分為:靜態代理和動態代理(動態代理又分為:JDK動態代理和CGLIB動態代理)

    至於什麼不同,接下來正式我們著重要學習的內容

 


 

靜態代理

什麼是靜態代理呢?

  • 顧名思義,就是靜態的,死的,一旦創建了就不允許修改了或者說很難修改(指的是程式運行期間)
  • 專業一些的解釋
    • 如若代理類在程式運行之前就已經存在,那麼這種代理方式被稱為靜態代理。這種情況下的代理類通常都是我們在Java程式碼中定義的。
    • 通常情況下靜態代理類和目標類通常都會實現同一個介面或者派生自同一父類  

具體實現:

被代理類與代理類共同實現的介面

package cn.arebirth.staticproxy;    /**   * 需要實現的共同介面
* 因為要保證代理類要不改變被代理類原來功能的基礎上增加新的功能
*/ public interface RentalHouse { /** * 出租房子 */ void rent(); }

被代理類(房東)

package cn.arebirth.staticproxy;    /**   * 房東(目標類、被代理類)   */  public class Host implements RentalHouse {        @Override      public void rent() {          System.out.println("我是房東,出租500平米的大房子");      }  }

代理類(中介)

package cn.arebirth.staticproxy;    /**   * 靜態代理類(中介、代理類)   * 注意:需要被代理類實現相同的介面   */  public class StaticProxy implements RentalHouse {      private RentalHouse rentalHouse;        public StaticProxy(RentalHouse rentalHouse) {          this.rentalHouse = rentalHouse;      }        @Override      public void rent() {          System.out.println("我是中介,收你500推薦費");          //調用被代理類的租房方法          rentalHouse.rent();          System.out.println("我是中介,我又想收你1000塊錢!");      }  }

測試類

package cn.arebirth.staticproxy;    public class Test {      public static void main(String[] args) {          //創建被代理度下行          Host host = new Host();          /**           * 創建代理對象,並把被代理對象傳遞給代理對象,           * 因為他們都實現相同的介面,實現了相同的方法,這樣的話傳遞的對象可以是房東1 房東2 ...           */          StaticProxy proxy = new StaticProxy(host);          proxy.rent();      }  }      輸出結果:      我是中介,收你500推薦費      我是房東,出租500平米的大房子      我是中介,我又想收你1000塊錢!

 

試想一下,如果有兩個房東,三個,四個,甚至更多個房東的話,我們怎麼寫?

 

被代理類

package cn.arebirth.staticproxy;    /**   * 房東1(目標類、被代理類)   */  public class Host1 implements RentalHouse {        @Override      public void rent() {          System.out.println("我是房東1,出租500平米的大房子");      }  }      package cn.arebirth.staticproxy;    /**   * 房東2(目標類、被代理類)   */  public class Host2 implements RentalHouse {        @Override      public void rent() {          System.out.println("我是房東2,出租500平米的大房子");      }  }

 

代理類(中介)

package cn.arebirth.staticproxy;    /**   * 靜態代理類(中介、代理類)   * 注意:需要被代理類實現相同的介面   */  public class StaticProxy implements RentalHouse{      //什麼價位的房子      private int moneuy;        public Agent(int moneuy) {          this.moneuy = moneuy;      }        @Override      public void renting() {          //出租房東的租房          //中介調用的租房方法仍然是房東的租房方法          System.out.println("收取50元推薦費");          if (moneuy <= 800) {//金額小於等於800的時候              Host1 host = new Host1();              host.rent();          } else {              Host2 host = new Host2();              host.rent();          }            System.out.println("收取500元押金費用");      }  }

測試類

public class Test{      public static void main(String[] args) {          StaticProxy proxy= new StaticProxy(1000);          proxy.renting();      }  }      輸出結果:
   我是房東2,出租500平米的大房子

 

靜態代理的缺點:

  我們仔細來觀察下,隨著我們的被代理對象的增多,也是就是房東越來越多,那我們的被代理類就會越來越冗餘,中介的壓力也就會越來越大。

 


 

動態代理

  常用的動態代理又分為JDK動態代理和CGLIB動態代理

那麼兩者的使用場景又是什麼呢??

  如果目標對象實現了介面,就是上面我們舉到的例子,房東和中介共同實現的介面類似,這樣的話就採用JDK動態代理

  如果目標對象沒有實現介面,必須採用CGLIB動態代理

 

具體實現:

 

要實現的共同介面

package cn.arebirth.jdkproxy;    /**   * 需要實現的共同介面   */  public interface RentalHouse {      /**       * 出租房子       */      void rent();  }

房東(被代理對象)

package cn.arebirth.jdkproxy;    /**   * 房東(目標類、被代理類)   */  public class Host implements RentalHouse {        @Override      public void rent() {          System.out.println("我是房東,出租500平米的大房子");      }  }

核心來了!(JDK動態代理實現類)package cn.arebirth.jdkproxy;

import java.lang.reflect.InvocationHandler;  import java.lang.reflect.Method;  import java.lang.reflect.Proxy;    /**   * JDK動態代理類   */  public class JdkProxy implements InvocationHandler {
  //接收要被代理的類 RentalHouse rentalHouse;
public JdkProxy(RentalHouse rentalHouse) { this.rentalHouse = rentalHouse; } /** * @return 執行該方法就會產生代理類 */ public Object getProxy() { /** * Proxy.getProxyInstance(param1,param2,para3); * 參數一:傳入一個classLoader()對象 * 參數二:傳入被代理類所實現的介面的數組,因為代理類底層要根據介面產生 * 參數三:參數類型是InvocationHandler即可,this即當前類,我們當前類實現了此介面 */ Object proxy = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{RentalHouse.class}, this); return proxy; } /** * 這個方法類似於上面我所講述的靜態代理模式裡面的中介類的rent()方法 * @param proxy 代理對象--中介 * @param method 代理對象中的方法 * @param args 代理對象方法中的參數 * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("我是中介,收你500推薦費"); Object invoke = method.invoke(rentalHouse, args); System.out.println("我是中介,我又想收你1000塊錢!"); //返回執行方法後的返回值 return invoke; } }

測試類

package cn.arebirth.jdkproxy;    public class Test {      public static void main(String[] args) {          //創建JdkProxy動態代理對象類,並把需要被代理的對象傳遞進去          JdkProxy jdkProxy = new JdkProxy(new Host());            //獲得代理類 這裡一定要寫他們共同實現的介面  利用java多態的特性,如果直接寫真實類型是會報錯的          RentalHouse  proxy= (RentalHouse)jdkProxy.getProxy();          proxy.rent();      }  }

輸出結果:
  我是中介,收你500推薦費   我是房東,出租500平米的大房子   我是中介,我又想收你1000塊錢!

 

我想當你看到這裡一定會感到困惑,為什麼我們獲取到了代理對象後執行的執行代理對象的方法,明明是房東的方法,怎麼顯示的好像是JDK動態代理類裡面的invoke()方法??或許還有其他的困惑,我們將在下邊一一講解如何實現的原理。

既然想知道如何實現的,那麼我們就要從底層出發,來看看,底層的Proxy代理到底幫我們生成了一個怎樣的代理類。

再開始之前我希望接下來的程式碼與操作你是跟著我同步進行的,這樣子才會更深刻有更好的理解,當然不排除你是個人腦機器模擬器

開始吧!

 

我們想要看底層如何實現的,那麼我們首先就要獲得代理類的class文件

下面是我寫的一個獲取JDKProxy動態代理所生成的代理文件的工具類

 

package cn.arebirth.jdkproxy;    import sun.misc.ProxyGenerator;    import java.io.File;  import java.io.FileOutputStream;  import java.io.FileWriter;  import java.io.IOException;    public class ProxyUtil {      /**       * @param proxyName  生成文件代理類的名字       * @param interClass 代理類所實現的介面的class數組形式       * @param path       寫出路徑       */      public static void writeProxyClassToHardDisk(String proxyName, Class[] interClass, String path) {          byte[] bytes = ProxyGenerator.generateProxyClass(proxyName, interClass);            FileOutputStream out = null;            try {              out = new FileOutputStream(path + File.separator + proxyName+".class");              out.write(bytes);              out.flush();          } catch (IOException e) {              e.printStackTrace();          } finally {              try {                  if (out != null)                      out.close();              } catch (IOException e) {                  e.printStackTrace();              }          }      }  }

然後我們在測試類裡面使用它

package cn.arebirth.jdkproxy;    public class Test {      public static void main(String[] args) {          //創建JdkProxy動態代理對象類,並把需要被代理的對象傳遞進去          JdkProxy jdkProxy = new JdkProxy(new Host());            //獲得代理類          RentalHouse proxy = (RentalHouse) jdkProxy.getProxy();          proxy.rent();          //寫入JDK動態代理生成的代理類          ProxyUtil.writeProxyClassToHardDisk(proxy.getClass().getSimpleName(), proxy.getClass().getInterfaces(), "F:\Tools\DevelopmentTools");      }  }

 

最後生成出來的代理類是一個class位元組碼文件,我們需要使用反編譯工具來查看,我使用的是Luten(這個工具打開方式特殊,百度自行查找)

import cn.arebirth.jdkproxy.*;  import java.lang.reflect.*;    public final class $Proxy0 extends Proxy implements RentalHouse  {      private static Method m1;      private static Method m3;      private static Method m2;      private static Method m0;        public $Proxy0(final InvocationHandler invocationHandler) {          super(invocationHandler);      }        public final boolean equals(final Object o) {          try {              return (boolean)super.h.invoke(this, $Proxy0.m1, new Object[] { o });          }          catch (Error | RuntimeException error) {              throw;          }          catch (Throwable t) {              throw new UndeclaredThrowableException(t);          }      }        public final void rent() {//這個就是編譯器自動為我們實現了的介面中的方法          try {              super.h.invoke(this, $Proxy0.m3, null); //並調用了invoke          }          catch (Error | RuntimeException error) {              throw;          }          catch (Throwable t) {              throw new UndeclaredThrowableException(t);          }      }        public final String toString() {          try {              return (String)super.h.invoke(this, $Proxy0.m2, null);          }          catch (Error | RuntimeException error) {              throw;          }          catch (Throwable t) {              throw new UndeclaredThrowableException(t);          }      }        public final int hashCode() {          try {              return (int)super.h.invoke(this, $Proxy0.m0, null);          }          catch (Error | RuntimeException error) {              throw;          }          catch (Throwable t) {              throw new UndeclaredThrowableException(t);          }      }      
//在類第一次載入的時候,給全局變數進行初始化,看看裡面有沒有我們稍微眼熟的東西
static { try { $Proxy0.m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); $Proxy0.m3 = Class.forName("cn.arebirth.jdkproxy.RentalHouse").getMethod("rent", (Class<?>[])new Class[0]);//這裡就是我們所實現的介面中的方法 $Proxy0.m2 = Class.forName("java.lang.Object").getMethod("toString", (Class<?>[])new Class[0]); $Proxy0.m0 = Class.forName("java.lang.Object").getMethod("hashCode", (Class<?>[])new Class[0]); } catch (NoSuchMethodException ex) { throw new NoSuchMethodError(ex.getMessage()); } catch (ClassNotFoundException ex2) { throw new NoClassDefFoundError(ex2.getMessage()); } } }

在看到這裡的時候,我們大概能了解到,原來我們在使用我們代理類調用租房的方法的時候,它裡面並沒有我們寫的程式碼而是執行了一個

 super.h.invoke(this, $Proxy0.m3, null);

那麼super是誰?就是它自動繼承的Proxy類

那麼h是什麼?就是Proxy類的InvocationHandler

 

 那麼我們看這個InvocationHandler類是不是感覺有那麼一丟丟的眼熟啊,回過頭看,這就是我們在寫JDK動態代理類的時候實現的那個介面。

!!!!!!!!!!!!!

然後它又調用了invoke(this, $Proxy0.m3, null);

第一個參數:代理類對象

第二個參數:要執行的代理類對象的方  Method對象,這個Method對象的值已經由我們的編譯器幫我們構建好了,我們只需要第一個次載入這個類的時候他就會自動賦值了(static程式碼塊)

第三個參數:方法裡面的參數,當沒有參數的時候就是NULL

 

 

帶著你的疑惑,從頭在開始好好敲一遍程式碼,捋一遍,相信我,你將有更大的收貨!!

下面是簡略畫的思路圖,試著用畫圖來畫出你的思維

 

 

 

那麼說完了這個JDK動態代理,我們有沒有發現什麼缺點??找茬時間開始

  是不是這個JDK動態代理必須要依賴介面才能實現,如果沒有介面的話,那麼JDK動態代理也就涼涼了對吧,!

 


 

那麼好,接下來我們將說一種更加強大的動態代理方式CGLIB,它的實現原理只要我們懂了JDK動態代理,那麼下面的就是小兒科啦哈哈~

 

CGLIB的代理將不再需要介面也可以生成代理類,但是它需要導包!

 

 當然,你也可以選擇其他版本的JAR包!

===

開始

 

被代理類

package cn.arebirth.cglibproxy;    /**   * 被代理類   */  public class Host {      public void rent() {          System.out.println("Host:rental house");      }  }

生成代理對象的cglib類

package cn.arebirth.cglibproxy;    import net.sf.cglib.proxy.Enhancer;  import net.sf.cglib.proxy.MethodInterceptor;  import net.sf.cglib.proxy.MethodProxy;    import java.lang.reflect.Method;    public class CglibProxy implements MethodInterceptor {        /**       * 獲取代理類       *       * @return       */      public Object getProxy() {          Enhancer enhancer = new Enhancer();            //設置被代理對象          enhancer.setSuperclass(Host.class);          //設置回調方法  當前對象          enhancer.setCallback(this);            //創建代理對象          Object o = enhancer.create();          return o;      }        /**       * @param o           被代理對象       * @param method      被代理對象方法       * @param objects     被代理對象方法中的參數       * @param methodProxy 代理類中的方法       * @return       * @throws Throwable       */      @Override      public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {          System.out.println("中介收取手續費");          Object o1 = methodProxy.invokeSuper(o, objects);          System.out.println("中介收取中介費");          return o1;      }  }

測試類

package cn.arebirth.cglibproxy;    public class Test {      public static void main(String[] args) {          CglibProxy cglibProxy = new CglibProxy();          Host proxy = (Host) cglibProxy.getProxy();//強制轉換為我們的房主          proxy.rent();      }  }    輸出結果:    中介收取手續費    Host:rental house    中介收取中介費

 

我們動態代理的最大好處就是,可以在沒有介面的情況下,只有一個類,我們就可以動態的代理,在程式運行的時候動態的為他創建代理類


 

 

最後讓我們大概的總結下:

 

代理模式:
靜態代理
動態代理:JDK動態代理 CGLIB動態代理

代理模式的三個要素
A.抽象的類或介面 完成一件怎樣的事情
B 被代理對象 事情操作具體內容
C 代理對象 幫助我們完成事情的同時 可以增加其他的東西

具體的列子:我們找中介租房子
A 抽象的類或者介面 租房子
B 被代理對象 房東
C 代理對象 中介

代理模式的好處
A 房東可以安心的做自己的事情 (被代理對象可以做自己的事情)
B 我們有了問題可以直接找中介 (被代理對象變得比較安全)
C 可以增強程式碼的擴展性

JDK動態代理和CGLIB動態代理的使用場景

我們可以這樣記一下,只要被代理類沒有實現介面,我們就必須使用CGLIB動態代理