1. 程式人生 > 程式設計 >"秒懂"代理模式 -- java

"秒懂"代理模式 -- java

分享人: 元哥

1、代理模式

  • 設計模式中的結構型設計模式

  • 從字面意思理解,代理即A替B完成某件事。例如媒婆幫靚仔找一個廣州的女朋友,那麼媒婆就是代理方,代理的事情就是幫忙介紹男女朋友;靚仔就是委託方,委託的事情就是找一個廣州的女朋友。

1)、靜態代理

  • 靜態代理類圖

    image.png

  • Code實現

image.png

image.png

image.png
image.png
image.png

  • 在這裡我們會先宣告一個Target介面,宣告的方法就是findGirlFriend。
  • 然後宣告一個類Handsome表示靚仔,實現Target介面的方法findGirlFriend
  • 接著宣告一個類MiddleProxy表示媒婆,持有一個目標介面Target的物件,並且實現Target介面的方法。在這個方法中呼叫持有物件的方法的前後進行增強。增強的效果就是本來靚仔得自己一個人去做找女朋友這件事;但有了媒婆就先讓她展示她的資源,然後靚仔找一個自己喜歡的廣州的女朋友,最後媒婆給聯絡方式。
優點
  • 在呼叫的時候我們不用知道MiddleProxy是如何實現的,只需要知道被代理即可。對例子而言,靚仔不需要知道媒婆這個代理是怎麼幫忙找女朋友的,反正有了媒婆就能找到女朋友。
缺點
  • 代理類和委託類實現了相同的介面,代理類和委託類實現了相同的方法。如果介面增加一個方法,除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了程式碼維護的複雜度。

    • 舉個例子。上述的需求是靚仔要找個廣州的女朋友,那現在假如靚仔還有個特殊的需求,要找個廣州的男朋友,委託類Handsome和代理類Proxy都需要實現findBoyFriend()方法。

      image.png

  • 靜態代理類只能為特定的介面服務。如想要為多個介面服務則需要建立很多個代理類。

    • 如上的程式碼是隻為HandsomeMan類的訪問提供了代理,但是如果還要為其他類如PrettyGril類提供代理的話,就需要我們再次新增代理PrettyGril的代理類。

      image.png

    • 在舉個開發中的例子:在呼叫具體實現類之前,需要列印日誌等資訊,這樣我們只需要新增一個代理類,在代理類中新增列印日誌的功能,然後呼叫實現類,這樣就避免了修改具體實現類。滿足我們所說的開閉原則。但是如果想讓每個實現類都新增列印日誌的功能的話,就需要新增多個代理類,以及代理類中各個方法都需要新增列印日誌功能。

2)、動態代理

a)、jdk動態代理
  • 動態代理類圖

image.png

  • Code實現

image.png

image.png

image.png

image.png

  • 同樣的使用前面的例子,需求是靚仔讓媒婆幫忙找女朋友。因此Target介面、Handsome類不變。
  • 但在實現MiddleProxy媒婆這個類的時候,會去實現InvocationHandler這個類,實現的方法就是invoke方法和一個newProxyInstance方法。newProxyInstance是通過Java的反射類Proxy生成一個引數傳進來物件的代理;invoke方法就是成員屬性target中的所有成員方法進行增強。(可能你們會有很多疑問點,會在後面一一描述解決)
解決靜態代理的問題
  • 當有新使用動態代理

    • 對比類圖

    image.png

    image.png

    • 1)使用靜態代理,當HandsomeMan類中有新的方法例如findBoyFriend()需要代理,相應findBoyFriend()方法需要在MiddleProxy類中進行實現;而使用動態代理,在MiddleProxy中不需要實現

    • 2)

      ![image-20191212212233267](/Users/cvter/Library/Application Support/typora-user-images/image-20191212212233267.png)

      image.png

      • 使用靜態代理。當有新的委託方時,例如靚女也需要找媒婆介紹男朋友,這時候有個PrettyGirl類,那麼由於先前的Target介面類中宣告的方法是findGrilFriend,而我們需要的是findBoyFriend,Target介面不再適用,因此會導致MidlleProxy方法不再適用,需要新建新的目標介面和代理類來滿足需求。
      • 使用動態代理,newProxyInstance無論傳進來的物件是什麼,持有的物件始終是Object,就只需要再寫新的介面,不要需要再寫新的代理類。
JDK動態代理是怎麼實現的

image.png

image.png

  • 我們來對比靜態代理和動態代理。在靜態代理中target物件一個是MiddleProxy類的物件,而在動態代理中target物件是$Proxy0類的物件。

  • 我們知道靜態代理的MiddleProxy類是怎麼做的,因為是我們宣告的;但$Proxy0類原先是不存在,但是他是在呼叫newProxyInstance方法後生成,而方法中實現的是通過反射類Proxy.newProxyInstance。

    public Object newProxyInstance(Object target) {
    	this.target = target;
    	return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
    }
    複製程式碼
  • 我們來通過特殊的方式,將$Proxy0類的位元組碼也就是class輸出到檔案,然後經過idea反編譯得到以下

    public final class $Proxy0 extends Proxy implements Target {
        private static Method m1;
        private static Method m2;
        private static Method m3;
        private static Method m0;
        public $Proxy0(InvocationHandler var1) throws  {...}
        public final boolean equals(Object var1) throws  {...}
        public final String toString() throws  {...}
        public final void findGrilFriend() throws  {
            try {
                super.h.invoke(this,m3,(Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
        public final int hashCode() throws  {...}
        static {
            try {
                m1 = Class.forName("java.lang.Object").getMethod("equals",Class.forName("java.lang.Object"));
                m2 = Class.forName("java.lang.Object").getMethod("toString");
                m3 = Class.forName("proxy.dynamic_proxy.Target").getMethod("findGrilFriend");
                m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            } catch (NoSuchMethodException var2) {
                throw new NoSuchMethodError(var2.getMessage());
            } catch (ClassNotFoundException var3) {
                throw new NoClassDefFoundError(var3.getMessage());
            }
        }
    }
    複製程式碼
    • 程式碼有點兒多我們不要慌。首先我們看到$Proxy0類繼承Proxy,並實現Target介面,那麼它肯定實現了findGrilFriend方法。

    • 那麼我們來大膽猜想,$Proxy0類是通過反射類Proxy.newProxyInstance()生成的,方法接收的引數就是Target介面物件,那麼如果方法接收的引數是其他介面物件,例如BTarget介面物件,裡面實現的方法findBoyFriend,那麼$Proxy0類就會去實現BTarget介面的方法。因此可以認為,動態代理是靜態代理的升級版,在程式執行過程中會動態根據傳入的介面物件,動態生成指定介面物件的代理類$Proxy0的物件。

    • 接下來,我們繼續看findGrilFriend方法,呼叫的是 super.h.invoke(this,(Object[])null); super.h 是 Proxy類的成員屬性(InvocationHandler),在Proxy.newProxyInstance()生成$Proxy0會傳this物件。

      public $Proxy0(InvocationHandler var1) throws  {
      	super(var1);
      }
      複製程式碼
      public class MiddleProxy implements InvocationHandler {
        public Object newProxyInstance(Object target) {
          this.target = target;
          return Proxy.newProxyInstance(target.getClass().getClassLoader(),this);
        }
        ....
      }
      複製程式碼
    • m3是宣告的findGrilFriend()方法

      image.png

    • 總結一下過程:

      • a)經過反射類Proxy.newProxyInstance()生成的了$Proxy0類和物件,並且$Proxy0物件持有MiddleProxy物件。
      • b)當呼叫findGirlFriend方法,呼叫的是$Proxy0類中的方法,super.h.invoke()
      • c)會再呼叫MiddleProxy類中的invoke方法,由於動態的傳進來的HandsomeMan物件,其中會呼叫Object obj = method.invoke(target,args);
優缺點
  • 優點:解決了靜態代理中冗餘的代理實現類問題。
  • 缺點:JDK 動態代理是基於介面設計實現的,如果沒有介面,會拋異常
b)、cglib動態代理
  • 類圖

    image.png

  • 代理類

    image.png

  • 動態生成具體的代理類code

public class HandsomeMan$$EnhancerByCGLIB$$596495c6 extends HandsomeMan implements Factory {
  	...省略很多程式碼
    final void CGLIB$findGrilFriend$0() {
        super.findGrilFriend();
    }

    public final void findGrilFriend() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this,CGLIB$findGrilFriend$0$Method,CGLIB$emptyArgs,CGLIB$findGrilFriend$0$Proxy);
        } else {
            super.findGrilFriend();
        }
    }
}
複製程式碼

image.png

  • 總結一下流程;

    • 1)首先我們通過newProxyInstance動態生成代理類HandsomeMan$$EnhancerByCGLIB$$和物件

      public class MiddleProxy implements MethodInterceptor {
        public Object newProxyInstance(Object target){
          //工具類
          Enhancer enhancer=new Enhancer();
          //設定被代理的物件,也可以理解為設定父類,因為動態代理類是繼承了被動態代理類。
          enhancer.setSuperclass(target.getClass());
          //設定回撥函式
          enhancer.setCallback(this);
          //建立子類的動態代理類物件
          return enhancer.create();
        }
      ....
      }
      複製程式碼
    
    複製程式碼
  • 2)在子類HandsomeMan$$EnhancerByCGLIB$$物件呼叫findGirlFriend方法時,會呼叫MiddleProxy類的intercept方法

    • 3)在MiddleProxy類的intercept方法中,會在呼叫父類方法前後進行增強

      public class MiddleProxy implements MethodInterceptor {
        ....
        public Object intercept(Object o,Method method,Object[] objects,MethodProxy methodProxy) throws Throwable {
          System.out.println("這裡有很多單身的人.深圳、廣州、東莞你找一個喜歡的。");
          Object obj = methodProxy.invokeSuper(o,objects);
          System.out.println("給你喜歡的這個人的聯絡方式。");
          return obj;
        }
      }
      複製程式碼
區別
  • 由於 JDK 動態代理限制了只能基於介面設計,而對於沒有介面的情況,JDK 方式解決不了;

  • CGLib 採用了非常底層的位元組碼技術,其原理是通過位元組碼技術為一個類建立子類,並在子類中採用方法攔截的技術攔截所有父類方法的呼叫,順勢織入橫切邏輯,來完成動態代理的實現。

  • 但是 CGLib 在建立代理物件時所花費的時間卻比 JDK 多得多,所以對於單例的物件,因為無需頻繁建立物件,用 CGLib 合適,反之,使用 JDK 方式要更為合適一些。 同時,由於 CGLib 由於是採用動態建立子類的方法,對於 final 方法,無法進行代理。

優點:沒有介面也能實現動態代理,而且採用位元組碼增強技術,效能也不錯。 缺點:技術實現相對難理解些。

注意:對於從Object中繼承的方法,CGLIB代理也會進行代理,如hashCode()equals()toString()等,但是getClass()wait()等方法不會,因為它是final方法,CGLIB無法代理。