"秒懂"代理模式 -- java
分享人: 元哥
1、代理模式
-
設計模式中的結構型設計模式
-
從字面意思理解,代理即A替B完成某件事。例如媒婆幫靚仔找一個廣州的女朋友,那麼媒婆就是代理方,代理的事情就是幫忙介紹男女朋友;靚仔就是委託方,委託的事情就是找一個廣州的女朋友。
1)、靜態代理
-
靜態代理類圖
-
Code實現
- 在這裡我們會先宣告一個Target介面,宣告的方法就是findGirlFriend。
- 然後宣告一個類Handsome表示靚仔,實現Target介面的方法findGirlFriend
- 接著宣告一個類MiddleProxy表示媒婆,持有一個目標介面Target的物件,並且實現Target介面的方法。在這個方法中呼叫持有物件的方法的前後進行增強。增強的效果就是本來靚仔得自己一個人去做找女朋友這件事;但有了媒婆就先讓她展示她的資源,然後靚仔找一個自己喜歡的廣州的女朋友,最後媒婆給聯絡方式。
優點
- 在呼叫的時候我們不用知道MiddleProxy是如何實現的,只需要知道被代理即可。對例子而言,靚仔不需要知道媒婆這個代理是怎麼幫忙找女朋友的,反正有了媒婆就能找到女朋友。
缺點
-
代理類和委託類實現了相同的介面,代理類和委託類實現了相同的方法。如果介面增加一個方法,除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了程式碼維護的複雜度。
-
舉個例子。上述的需求是靚仔要找個廣州的女朋友,那現在假如靚仔還有個特殊的需求,要找個廣州的男朋友,委託類Handsome和代理類Proxy都需要實現findBoyFriend()方法。
-
-
靜態代理類只能為特定的介面服務。如想要為多個介面服務則需要建立很多個代理類。
-
如上的程式碼是隻為HandsomeMan類的訪問提供了代理,但是如果還要為其他類如PrettyGril類提供代理的話,就需要我們再次新增代理PrettyGril的代理類。
-
在舉個開發中的例子:在呼叫具體實現類之前,需要列印日誌等資訊,這樣我們只需要新增一個代理類,在代理類中新增列印日誌的功能,然後呼叫實現類,這樣就避免了修改具體實現類。滿足我們所說的開閉原則。但是如果想讓每個實現類都新增列印日誌的功能的話,就需要新增多個代理類,以及代理類中各個方法都需要新增列印日誌功能。
-
2)、動態代理
a)、jdk動態代理
- 動態代理類圖
- Code實現
- 同樣的使用前面的例子,需求是靚仔讓媒婆幫忙找女朋友。因此Target介面、Handsome類不變。
- 但在實現MiddleProxy媒婆這個類的時候,會去實現InvocationHandler這個類,實現的方法就是invoke方法和一個newProxyInstance方法。newProxyInstance是通過Java的反射類Proxy生成一個引數傳進來物件的代理;invoke方法就是成員屬性target中的所有成員方法進行增強。(可能你們會有很多疑問點,會在後面一一描述解決)
解決靜態代理的問題
-
當有新使用動態代理
- 對比類圖
-
1)使用靜態代理,當HandsomeMan類中有新的方法例如findBoyFriend()需要代理,相應findBoyFriend()方法需要在MiddleProxy類中進行實現;而使用動態代理,在MiddleProxy中不需要實現
-
2)
![image-20191212212233267](/Users/cvter/Library/Application Support/typora-user-images/image-20191212212233267.png)
- 使用靜態代理。當有新的委託方時,例如靚女也需要找媒婆介紹男朋友,這時候有個PrettyGirl類,那麼由於先前的Target介面類中宣告的方法是findGrilFriend,而我們需要的是findBoyFriend,Target介面不再適用,因此會導致MidlleProxy方法不再適用,需要新建新的目標介面和代理類來滿足需求。
- 使用動態代理,newProxyInstance無論傳進來的物件是什麼,持有的物件始終是Object,就只需要再寫新的介面,不要需要再寫新的代理類。
JDK動態代理是怎麼實現的
-
我們來對比靜態代理和動態代理。在靜態代理中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()方法
-
總結一下過程:
- 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動態代理
-
類圖
-
代理類
-
動態生成具體的代理類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();
}
}
}
複製程式碼
-
總結一下流程;
-
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無法代理。