設計模式——代理模式(超級詳細的代理模式講解)
本文首發於cdream的個人博客,點擊獲得更好的閱讀體驗!
歡迎轉載,轉載請註明出處。
本文主要對設計模式中的代理模式進行講解,包括靜態代理舉例,動態代理中的jdk動態代理、cglib動態代理原理分析等幾個方面。
一、概念
定義:代理模式(Proxy Pattern)代理模式給某一個對象提供一個代理對象,並由代理對象控制對原對象的訪問。代理對象在客戶端了和目標對象之間起到中介作用。
二、結構
UML:
主要角色:
真實主題類:客戶端真正想調用的的主題類。
代理類:保存一個真實主題類的引用,使得代理對象可以訪問真實主體對象的實體。真實主題類和代理對象都會繼承相同的接口:在用到真實主題類的地方都可以使用代理類來完成。
抽象主體:定義真實主題類和代理類的接口。
三、靜態代理
虛擬代理
虛擬代理作為創建開銷大的對象的代表。直到我們真正使用對象時才會創建它,當對象在創建前和創建中,由虛擬代理來扮演對象的替身。對象創建後,代理會將請求直接委托給對象。
抽象主題接口
public interface LargeObject {
/**
* 幹了一個老大的事了,所以我這個類老大了!
*/
public void doBigThing();
}
具體主題角色,實現了抽象主題接口
public class RealLargeObject implements LargeObject { @Override public void doBigThing() { System.out.println("做了老大的一個事了"); } }
現在我們要對類進行訪問控制,對"巨型對象"進行延遲創建。
public class ProxyLargeObject implements LargeObject {
private LargeObject largeObject;
@Override
public void doBigThing() {
if (largeObject==null){
largeObject = new RealLargeObject();
}
largeObject.doBigThing();
}
}
當時使用代理類時,只有當客戶端調用doBigThing方法的時候才會創建LargeObject對象。
當我們需要對開銷大的對象進行延遲創建或隱藏其創建過程時可以使用虛擬代理模式。
遠程代理
概述
RMI
遠程代理RMI允許一個jvm上的對象調用另一個jvm上的對象,流程類似於上面這個圖。
客戶對象直接調用輔助對象 stub的方法,stub打包調用信息,通過網絡把他運送給服務端輔助對象 skeleton,服務端輔助對象進行解包,調用真正服務對象的真正方法。
然後服務對象將結果返回給服務輔助對象,服務輔助對象將結果打包,然後客戶服務對象將返回結果解包交給真正客戶對象。
由registry來作為註冊中心,服務端將對象註冊到其中,客戶端通過相關方法調用。
源代碼
下面來介紹RMI步驟
1.制作遠程接口
public interface MyRemote extends Remote {
// 這裏由於是網絡調用,肯定要面遠程調用風險
public String sayHello() throws RemoteException;
}
2.制作遠程實現
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
private static final long serialVersionUID = 6780156706603775814L;
protected MyRemoteImpl() throws RemoteException {
super();
}
@Override
public String sayHello() throws RemoteException {
return "hahahahaha";//真實的服務
}
public static void main(String[] args) {
try {
// 創建遠程服務對象
MyRemote myRemote = new MyRemoteImpl();
// 綁定遠程服務對象到 rmiregistry
Naming.rebind("RemoteServer", myRemote);
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.產生Stub和Skeleton
對遠程實現類執行,到classes目錄下找到遠程實現類,然後使用如下命令rmic com.rmidemo.MyRemoteImpl
產生stub和skeleton文件。
4.啟動registry
依然是在classes 目錄下運行 remiregistry 命令啟動registry
5.啟動服務
運行遠程服務實現的 main() 方法 —> MyRemoteImpl
public class MyRemoteClient {
public static void main(String[] args) {
new MyRemoteClient().go();
}
public void go() {
try {
MyRemote myRemote = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteServer”);// RemoteServer 就是註冊(rebind)時的 key
String hello = myRemote.sayHello();
System.out.println(hello);
} catch (Exception e) {
e.printStackTrace();
}
}
}
這樣就完成了RMI的調用,這種方法其實已經過時了,但是這是一個標準的遠程代理模式,客戶端調用的是註冊中心中的遠程實現類的代理,但就像是調用本地的方法一樣,正常使用。
以上介紹了,兩種靜態代理模式,在編寫代碼時就被創建好了,代理類和被代理類實現了相同的接口,但這樣帶來的問題就是一旦接口添加新的方法,就需要對代理類和被代理類進行修改。此外,如果要針對不同類的不同方法實現相同增強,則需要創建多個代理類。就要為了解決這些問題,java引入了動態代理!四、動態代理
動態代理有兩種,一種是jdk動態代理,另一種是cglib動態代理,前者是根據繼承當前類的接口,而cglib是繼承當前類。所以jdk動態代理無法對沒有實現接口的類進行代理。
jdk動態代理
UML:
主題對象
// 定義一個人的接口,用來創建代理~
public interface Person {
// 移動
void move();
// 獲取名字
String getName();
}
真正主題對象,就是我Cderam,移動只能靠走路
public class Cdream implements Person {
private String name;
private String desc;
public Cdream() {
}
public Cdream(String name, String desc, String state) {
this.name = name;
this.desc = desc;
}
@Override
public void move(){
System.out.println("我現在移動只能靠走路!");
}
@Override
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
現在我要開始通過動態代理來改裝自己了!!
// 搞一個handler
public class CdreamInvocationHandler implements InvocationHandler {
private Cdream cdream;
public CdreamInvocationHandler(Cdream cdream) {
this.cdream = cdream;
}
// proxy 代理對象
// method 調用方法
//
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 調用方法呢
Object invoke = method.invoke(cdream, args);
// 這個看起來太像Spring的Aop了,有木有?
if (method.getName().startsWith("move")) {
System.out.println("我要通過代理類給自己加技能了!");
System.out.println("穿上鋼鐵戰衣,化身鋼鐵俠");
System.out.println("老子會飛了!哼!");
}
return invoke;
}
}
invoke方法中的proxy的用途:1.可以使用反射獲取代理對象的信息(也就是proxy.getClass().getName())
2.可以將代理對象返回以進行連續調用,這就是proxy存在的目的,因為this並不是代理對象。
測試類
public class Test {
public static void main(String[] args) {
Cdream cdream = new Cdream("在水一方","java的粉絲");
// 創建一個handler
CdreamInvocationHandler handler = new CdreamInvocationHandler(cdream);
// 創建代理類
Person o = (Person)Proxy.newProxyInstance(Cdream.class.getClassLoader(), cdream.getClass().getInterfaces(),
handler);
cdream.move();
System.out.println("---------");
o.move();
System.out.println("---------");
System.out.println(cdream.getName());
System.out.println(o.getName());
}
}
******輸出結果*********
我現在移動只能靠走路!
---------
我現在移動只能靠走路!
我要通過代理類給自己加技能了!
穿上鋼鐵戰衣,化身鋼鐵俠
老子會飛了!哼!
---------
在水一方
在水一方
看,我就這樣穿上了鋼鐵戰衣,化身鋼鐵俠,能飛上天與太陽肩並肩了!
這就是動態代理的功能,其實是把我加強了啊~~~~本來代理模式的目的是用來控制訪問的,結果讓我弄成加強自己了……有點像裝飾者模式了,不過大家對付看吧,用法基本就是這個樣子的。
看到這裏一定有人要提問題了:“等等等等,我好像在哪裏見過這個模式啊,哇!!!!這不是裝飾者模式嗎?小樣,別以為你穿上馬甲我就不認識你了!“
唉,其實我也覺得這兩個模式太像了!然後我就谷歌啊百度,努力找到了這兩個模式的不同,開始做筆記吧!
代理模式與裝飾者模式的不同:
然後來看看代理類的具體信息,對jdk動態代理進行深入研究!
public class Test {
public static void main(String[] args) {
Cdream cdream = new Cdream("在水一方","java的粉絲","狂熱");
CdreamInvocationHandler handler = new CdreamInvocationHandler(cdream);
Person o = (Person)Proxy.newProxyInstance(Cdream.class.getClassLoader(), cdream.getClass().getInterfaces(),
handler);
System.out.println("o是Proxy的實例:"+(o instanceof Proxy));
System.out.println("o真正的類名:"+o.getClass().toString());
System.out.println("o實現了哪些接口:"+Arrays.toString(o.getClass().getInterfaces()));
System.out.println("o的超類是:"+o.getClass().getSuperclass());
System.out.print("o中的屬性有:");
Field[] field=o.getClass().getDeclaredFields();
for(Field f:field){
System.out.print(f.getName()+", ");
}
System.out.print("\n"+"o中的方法有:");
Method[] method=o.getClass().getDeclaredMethods();
for(Method m:method){
System.out.print(m.getName()+", ");
}
}
}
------------
o是Proxy的實例:true
o真正的類名:class com.sun.proxy.$Proxy0
o實現了哪些接口:[interface design.proxy_pattern.dynamic_proxy.Person]
o的超類是:class java.lang.reflect.Proxy
o中的屬性有:m1, m4, m3, m2, m0,
o中的方法有:equals, toString, hashCode, getName, move,
以上得出結論,創建出來的類是$Proxy0(動態生成的!),父類是Proxy,實現了接口Person,有四個看不懂的屬性,方法除了我Person接口裏的move、getName還多了幾個方法。然後我們一頭紮進newProxyInstance
看看到地方發生了什麽事。
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
// handler非空判斷
Objects.requireNonNull(h);
// 對接口進行克隆
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* Look up or generate the designated proxy class.
* 這個東西生成了一個新的Class,也就是我們剛看到的$Proxy0這個類
*/
Class<?> cl = getProxyClass0(loader, intfs);
/*
* Invoke its constructor with the designated invocation handler.
* 開始創建類了
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
// 創建了$Proxy0的對象,並傳入h為Proxy中的Handler賦值($Proxy繼承了Proxy)
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
這個方法幹了兩件事:
- 根據參數loader和interfaces調用方法 getProxyClass(loader, interfaces)創建代理類$Proxy0類 實現了interfaces的接口,並繼承了Proxy類。
- 實例化$Proxy0並在構造方法中把Handler傳過去,接著$Proxy0調用父類Proxy的構造器,為h賦值。
然後看看生成代理類$Proxy0
public final class Proxy0 extends Proxy implements Person {
private static Method m1;
private static Method m4;
private static Method m3;
private static Method m2;
private static Method m0;
public Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
// 就是在這裏調用的invoke啊!!!!
public final void move() throws {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String getName() throws {
try {
return (String)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m4 = Class.forName("design.proxy_pattern.dynamic_proxy.Person").getMethod("move");
m3 = Class.forName("design.proxy_pattern.dynamic_proxy.Person").getMethod("getName");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
我們把這個$Proxy轉成了Person,然後Person就調用這裏面的move方法,move方法觸發了CdreamInvocationHandler 中的invoke方法,所以我這次才有性穿上鋼鐵戰衣化身鋼鐵俠!!!
真是不禁會想到,我看個原理都看的這麽費勁,寫這東西的大佬得多可怕啊!!!
jdk動態代理的缺陷,大家也知道,就是這個只能對有接口的類進行代理,並且只有接口裏的方法能被增強,如果木有接口的類我們怎麽處理啊?emmmmmm……下面我們介紹cglib動態代理。
cglib動態代理
沿用上面的Cdream類
public class Cdream {
private String name;
private String desc;
public Cdream() {
}
public Cdream(String name, String desc, String state) {
this.name = name;
this.desc = desc;
}
@Override
public void move(){
System.out.println("我現在移動只能靠走路!");
}
@Override
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
創建一個方法攔截器,類似於jdk中的handler
public class CdreamInterceptor implements MethodInterceptor {
private Cdream cdream;
public CdreamInterceptor(Cdream cdream) {
this.cdream = cdream;
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 這類註意參數裏一個method,一個methodProxy,下面我進行解釋
Object invoke = methodProxy.invokeProxy(o, args);
if (method.getName().startsWith("move")){
System.out.println("穿上鋼鐵戰衣,化身鋼鐵狗");
}
return invoke;
}
}
測試類
public class test2 {
public static void main(String[] args) {
// 這個是cglib裏的增強類
Enhancer enhancer = new Enhancer();
// 設置超類
enhancer.setSuperclass(Cdream.class);
// 創建被增強類
Cdream cdream = new Cdream();
cdream.setName("dog");
cdream.setDesc("喜歡吃骨頭肉");
// 設置回調函數,類似於jdk中的Handler
enhancer.setCallback(new CdreamInterceptor(cdream));
Cdream o = (Cdream)enhancer.create();
System.out.println(o.getName());
cdream.move();
System.out.println("---------");
o.move();
}
}
--------
dog
我現在移動只能靠走路!
---------
我現在移動只能靠走路!
穿上鋼鐵戰衣,化身鋼鐵狗
這個是cglib動態代理的基本流程,然後我們對cglib進行深入了解一下
public class test2 {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Cdream.class);
Cdream cdream = new Cdream();
cdream.setName("dog");
enhancer.setCallback(new CdreamInterceptor(cdream));
Cdream o = (Cdream)enhancer.create();
System.out.println("o真正的類名:"+o.getClass().toString());
System.out.println("o實現為了哪些接口:"+ Arrays.toString(o.getClass().getInterfaces()));
System.out.println("o的超類是:"+o.getClass().getSuperclass());
System.out.print("o中的屬性有:");
Field[] field=o.getClass().getDeclaredFields();
for(Field f:field){
System.out.print(f.getName()+", ");
}
System.out.print("\n"+"o中的方法有:");
Method[] method=o.getClass().getDeclaredMethods();
for(Method m:method){
System.out.print(m.getName()+", ");
}
}
}
--------
o真正的類名:class design.proxy_pattern.dynamic_proxy.Cdream$$EnhancerByCGLIB$$b100d497
o實現為了哪些接口:[interface net.sf.cglib.proxy.Factory]
o的超類是:class design.proxy_pattern.dynamic_proxy.Cdream
o中的屬性有:CGLIB$BOUND, CGLIB$THREAD_CALLBACKS, CGLIB$STATIC_CALLBACKS, CGLIB$CALLBACK_0, CGLIB$getName$0$Method, CGLIB$getName$0$Proxy, CGLIB$emptyArgs, CGLIB$setName$1$Method, CGLIB$setName$1$Proxy, CGLIB$move$2$Method, CGLIB$move$2$Proxy, CGLIB$setDesc$3$Method, CGLIB$setDesc$3$Proxy, CGLIB$getDesc$4$Method, CGLIB$getDesc$4$Proxy, CGLIB$finalize$5$Method, CGLIB$finalize$5$Proxy, CGLIB$equals$6$Method, CGLIB$equals$6$Proxy, CGLIB$toString$7$Method, CGLIB$toString$7$Proxy, CGLIB$hashCode$8$Method, CGLIB$hashCode$8$Proxy, CGLIB$clone$9$Method, CGLIB$clone$9$Proxy,
o中的方法有:finalize, equals, toString, hashCode, clone, getName, newInstance, newInstance, newInstance, setName, move, setCallback, setCallbacks, CGLIB$SET_STATIC_CALLBACKS, CGLIB$SET_THREAD_CALLBACKS, getCallback, getCallbacks, setDesc, CGLIB$findMethodProxy, getDesc, CGLIB$STATICHOOK1, CGLIB$getName$0, CGLIB$BIND_CALLBACKS, CGLIB$setName$1, CGLIB$move$2, CGLIB$setDesc$3, CGLIB$getDesc$4, CGLIB$finalize$5, CGLIB$equals$6, CGLIB$toString$7, CGLIB$hashCode$8, CGLIB$clone$9,
我竟後悔把方法和屬性都打印出來了,這誰受的了啊!不管了,看重點,cglib的代理類確實繼承了被增強類,這樣就不必在意是否有接口了,但帶來的問題就是final方法無法增強,然後代理類又實現了Factory,用來創建實例使用。然後代理了好多好多方法~
然後我們來看看代理類
在調用方法創建代理類前把這個段代碼加上去,後面是輸出的位置
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"/Users/cdream/IdeaProjects/Project1/out");
下面是代理類裏的move方法
final void CGLIB$move$2() {
super.move();
}
public final void move() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
// 這個就是CallBack,上面創建實例Enhancer時設置進去了
if (this.CGLIB$CALLBACK_0 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
var10000.intercept(this, CGLIB$move$2$Method, CGLIB$emptyArgs, CGLIB$move$2$Proxy);
} else {
super.move();
}
}
代理類為每個方法都生成了一個CGLIB$move$2這樣的代理方法
下面這個move方法是被我們強轉後,會調用,如果CallBack為空的情況下,會調用父類中的方法,如果設置後就會調用方法攔截器中的方法(CdreamInterceptor.intercept)。
然後我們來說一下method和proxyMethod,下面這個是源代碼中的註釋
public interface MethodInterceptor
extends Callback
{
/**
* All generated proxied methods call this method instead of the original method.
* The original method may either be invoked by normal reflection using the Method object,
* or by using the MethodProxy (faster).
* @param obj "this", the enhanced object
* @param method intercepted Method
* @param args argument array; primitive types are wrapped
* @param proxy used to invoke super (non-intercepted method); may be called
* as many times as needed
* @throws Throwable any exception may be thrown; if so, super method will not be invoked
* @return any value compatible with the signature of the proxied method. Method returning void will ignore this value.
* @see MethodProxy
*/
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
MethodProxy proxy) throws Throwable;
}
method是被攔截的方法,如果進行調用,則需要使用method.invoke(cdream,args)
,利用反射機制進行調用,而methodProxy則是利用fastClass機制,調用時利用一個hash算法快速定位代理類中的方法,調用時使用如下的方式methodProxy.invokeSuper(o,args)
,methodProxy.invoke(cdream,args)
前者是調用代理類中的CGLIB$move$2()方法,後者是調用代理類中的move()方法。註意invokeSuper的第一個參數。
五、優缺點
優點:
代理模式能夠協調調用者和被調用者,在一定程度上降低了系統的耦合度;
代理對象可以在客戶端和目標對象之間起到中介的作用,這樣起到了保護目標對象的作用。
缺點:
- 由於在客戶端和真實對象之間增加了代理對象,因此有些類型的代理模式可能會造成請求的處理速度變慢;
- 實現代理模式需要額外的工作,有些代理模式的實現非常復雜;
六、總結
本文對代理模式簡要進行了介紹,在日常開發中,代理模式經常會被用到。
靜態代理模式與裝飾者模式類似,但目的是不同的,裝飾者模式目的是對類進行包裝,而代理模式則是為了控制對象的方法訪問權限。
動態代理是靜態代理的增強,避免創建過多的代理類。動態代理的代理類是在運行中生成的,而靜態代理的代理類需要我們事先編寫好。我們經常會用到的動態代理包括jdk動態代理和cglib動態代理,jdk動態代理需要被代理類的接口,cglib則是繼承被代理類,由於cglib的fastClass機制,cglib的效率會高於利用反射的jdk動態代理。
Spring中的AOP,微服務的RPC都是代理模式的一種形式。
參考資料:
- Head First 設計模式,Eric Freeman &Elisabeth Freeman with Kathy Sierra & Bert Bates
- Cglib源碼分析 invoke和invokeSuper的差別
- cglib源碼分析(四):cglib 動態代理原理分析
- Factory proxy pattern,wiki
- 說說cglib動態代理,占小狼
設計模式——代理模式(超級詳細的代理模式講解)