1、裝飾者模式與代理模式 (靜態代理)
在日常開發里面,我們經常需要給某個類的方法增加加某些特定的功能。
例如:有嬰兒,嬰兒會吃飯和走動,如以下類
1 package com.scl.designpattern.proxy; 2 3 //嬰兒類 4 public class Child implements Human 5 { 6 public void eat() 7 { 8 System.out.println("eat something...."); 9 } 10 11 @Override 12 public void run() 13 { 14 System.out.println("Child run very slow"); 15 } 16 }
突然有一天,家長發現不行,孩子不能隨便吃東西,而且吃飯前一定要洗手。但是孩子太小(被委托方),不會自己洗手。家長(Client 端)又沒法照顧孩子。那簡單,找個保姆照顧孩子! 讓保姆類和嬰兒類共同實現同一個接口,讓保姆類全程管理小孩,同時在家長眼里,只要看到保姆在幫孩子洗手就可以了。於是,有以下內容。
1 package com.scl.designpattern.proxy; 2 3 //保姆類 4 public class BabySitter implements Human 5 { 6 7 @Override 8 public void eat() 9 { 10 11 } 12 13 @Override 14 public void run() 15 { 16 17 } 18 19 }
現在保姆已經有了,孩子也有了,怎么把孩子跟保姆關聯起來。讓保姆給相應的孩紙洗手。於是保姆類更改如下
1 package com.scl.designpattern.proxy; 2 3 //保姆類 4 public class BabySitter implements Human 5 { 6 private Human human; 7 8 public BabySitter(Human human) 9 { 10 this.human = human; 11 } 12 13 @Override 14 public void eat() 15 { 16 // 添加washHand的方法 17 this.washHandForChild(); 18 human.eat(); 19 } 20 21 @Override 22 public void run() 23 { 24 25 } 26 27 public void washHandForChild() 28 { 29 System.out.println("help the child to wash his hands"); 30 } 31 }
好,那么家長就是給孩紙找了個保姆代理,讓他附加了一些嬰兒做不了事。同時家長也沒有強迫孩紙自己學會洗手(不更改Child類)
1 package com.scl.designpattern.proxy; 2 3 //客戶端 4 public class Client 5 { 6 public static void main(String[] args) 7 { 8 Human human = new BabySitter(new Child()); 9 human.eat(); 10 } 11 }
以上就是一個簡單的裝飾模式,來看一下這一塊完整的類圖。

裝飾模式的一個很重要特點就是,在客戶端可以看到抽象對象的實例,如Human human = new BabySitter(new Child()); 因為裝飾模式通過聚合方式,把內容整合到裝飾類里面了。
裝飾者模式能夠使用裝飾類對抽象對象進行裝飾。假如來了個OldMan類手腳不利索。保姆類BabySitter同樣能夠勝任這個OldMan的飯前洗手操作。
例子想了很久,也看了不少別人的博客。發現一個很令人迷惑的問題:為什么我用想的代理模式,確是別人口中的裝飾模式?裝飾者模式和代理模式有什么區別?。大致的代理模式類圖如下:

由該類圖可知,以上BabySitter代碼應該如下:
1 package com.scl.designpattern.proxy; 2 3 //保姆類 4 public class BabySitter implements Human 5 { 6 private Child child; 7 8 public BabySitter() 9 { 10 child = new Child(); 11 } 12 13 @Override 14 public void eat() 15 { 16 // 添加washHand的方法 17 this.washHandForChild(); 18 human.eat(); 19 } 20 21 @Override 22 public void run() 23 { 24 25 } 26 27 public void washHandForChild() 28 { 29 System.out.println("help the child to wash his hands"); 30 } 31 }
1 package com.scl.designpattern.proxy; 2 3 //客戶端 4 public class Client 5 { 6 public static void main(String[] args) 7 { 8 Human human = new BabySitter(); 9 human.eat(); 10 } 11 }
裝飾者模式和代理模式的最后運行的結果都是一樣的,顯示如下

由代理模式代碼可知,客戶端不關心代理類了哪個類。但代碼控制了客戶端對委托類的訪問。客戶端代碼表現為 Human human = new BabySitter( );
所以資料上都說了,裝飾模式主要是強調對類中代碼的拓展,而代理模式則偏向於委托類的訪問限制。兩者都一樣擁有抽象角色(接口)、真實角色(委托類)、代理類 。
由於代理類實現了抽象角色的接口,導致代理類無法通用。如有天,一個有錢人養了只小猩猩,他要一個保姆在猩猩吃東西前,幫猩猩洗手....保姆根本不懂猩猩的特性(跟猩猩類不是同一類型的,保姆屬於Human類,而猩猩可能屬於Animal類型。),但洗手這個方法是不變的,用水洗。能不能找一個代理說既可以照看人吃飯前洗手也可以照看猩猩吃飯前洗手?
用機器人吧... (編不下去了)。要實現這種功能,必須讓代理類與特定的接口分離。在代理的時候能夠了解每個委托的特性,這就有可能了。這時候動態代理就出現了。
2、動態代理
2.1 基於繼承接口類的動態代理
如靜態代理的內容所描述的,靜態代理受限於接口的實現。動態代理就是通過使用反射,動態地獲取抽象接口的類型,從而獲取相關特性進行代理。因動態代理能夠為所有的委托方進行代理,因此給代理類起個通用點的名字HealthHandle。先看保姆類可以變成什么樣。
1 package com.scl.designpattern.proxy.dynamic; 2 3 import java.lang.reflect.InvocationHandler; 4 import java.lang.reflect.Method; 5 import java.lang.reflect.Proxy; 6 7 public class HealthHandle implements InvocationHandler 8 { 9 10 private Object proxyTarget; 11 12 public Object getProxyInstance(Object target) 13 { 14 this.proxyTarget = target; 15 return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this); 16 } 17 18 @Override 19 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 20 { 21 Object methodObject = null; 22 System.out.println("proxy washing hand start"); 23 methodObject = method.invoke(proxyTarget, args); 24 System.out.println("proxy washing hand end"); 25 return methodObject; 26 } 27 }
1 package com.scl.designpattern.proxy.dynamic; 2 3 import java.lang.reflect.Proxy; 4 5 //客戶端 6 public class Client 7 { 8 public static void main(String[] args) 9 { 10 HealthHandle h = new HealthHandle(); 11 Human human = (Human) h.getProxyInstance(new Child()); 12 human.eat(); 13 human.run(); 14 } 15 }
首先,使用動態代理
1. 必須實現InvocationHandler接口,訂立一個契約標識該類為一個動態代理執行類。
2. InvocationHandler接口內有一實現方法簽名如下: public Object invoke(Object proxy, Method method, Object[] args) 。使用時需要重寫這個方法
3. 獲取代理類,需要使用 Proxy.newProxyInstance(Clas loader, Class<?>[] interfaces, InvocationHandler h) 這個方法去獲取Proxy對象(Proxy類類型的實例,非Child類類型實例)。
先看獲取代理對象簽名解析如下:
//Clas loader : 類的加載器 //Class<?>[] interfaces : 委托類實現的接口,保證代理類返回的是同一個實現接口下的類型,保持代理類與抽象角色行為的一致 //invocationHandler, 該類最重要的一環。一個設計模式:策略模式.即告訴代理類,代理類遇到某個委托類的方法時該調用哪個類下的invoke方法。 Proxy.newProxyInstance(Class loader, Class<?>[] interfaces, InvocationHandler h)
再看實現InvocationHandler方法下的簽名 public Object invoke(Object proxy, Method method, Object[] args)
//第一個參數為Proxy類類型實例,如匿名的$proxy實例 //第二個參數為委托類的方法對象//第三個參數為委托類的方法參數
//返回類型為委托類某個方法的執行結果 public Object invoke(Object proxy, Method method, Object[] args)
總的來說這個方法就是動態獲取委托類里面的方法,在調用委托類的方法時在這個方法內進行拓展。 通過上面的Proxy.newProxyInstance方法,告訴底層代碼,該去哪個類里面執行invoke方法。
由上面的描述,在客戶端代碼的調用后,結果如下:

由此可見,在代理類對系統下每個方法的調用時,都會去調用invocationHandler里面的invoke方法. 包括里面的eat和run方法,可見受作用的范圍是多廣:每個方法都受代理類作用了。由例子可以看出,如果要實現我們想要的方法上面添加特定的代理,可以通過invoke方法里面的方法反射獲取method對象方法名稱即可實現(但這樣會產生硬編碼)。修改可如下:
1 @Override 2 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 3 { 4 Object methodObject = null; 5 if (method.getName().equals("eat")) 6 { 7 System.out.println("proxy washing hand start"); 8 methodObject = method.invoke(proxyTarget, args); 9 System.out.println("proxy washing hand end"); 10 } 11 else 12 {
//不使用第一個proxy參數作為參數,否則會造成死循環 13 methodObject = method.invoke(proxyTarget, args); 14 } 15 return methodObject; 16 }
由此,我們可以猜想出來動態代理在開發中的用法:
1. 監控程序下執行某個方法的耗時、性能及日志的編寫
2. 監控程序方法的權限,不再單一地實行Controler→ServiceImpl→DaoImpl這種縱向編程。增加了在同一層面間橫向編程的可能(切面編程:AOP)
另外有幾點必須指出的內容:
1. 基於接口類的動態代理模式,必須具備抽象角色、委托類、代理三個基本角色。委托類和代理類必須由抽象角色衍生出來,否則無法使用該模式
2.在使用Invoke方法的時候,需要對method的可訪問性進行加工嗎?不需要,動態代理模式最后返回的是具有抽象角色(頂層接口)的對象。在委托類內被private或者protected關鍵修飾的方法將不會予以調用,即使允許調用。也無法在客戶端使用代理類轉換成子類接口,對方法進行調用。

3. 在invoke方法內為什么不使用第一個參數進行執行回調。
在客戶端使用getProxyInstance(new Child( ))時,JDK會返回一個$proxy的實例,實例內有InvokecationHandler對象及動態繼承下來的方法eat。客戶端調用了eat方法,有如下操作:①JDK先查找$proxy實例內的handler對象 ②執行handler內的invoke方法。 根據public Object invoke(Object proxy, Method method, Object[] args)這個方法簽名第一個參數proxy就是對應着下圖的$proxy實例。如果在invoke內使用method.invoke(proxy,args) ,會出現這樣一條方法鏈,eat→invoke→eat→invoke...,最終導致堆棧溢出。
2.2 基於子類的動態代理
假設目前有個項目,A公司寫了一個Computer類已經整合到某一個jar包里面,這個類是個普通類沒有任何抽象角色(沒有實現任何接口)。但是B公司需要對Computer類內的方法進行加強、拓展。那B公司怎么辦?目前只有兩個方法:1、拿A公司代碼反編譯,然后修改A公司里面這個類的方法。 2、拿A公司代碼反編譯,讓A公司Computer類進行抽象化,然后B公司使用基於接口的動態代理對方法進行拓展。很明顯,這兩個方法都是違背了面向對象設計的思想的 (①高內聚低耦合 ②開閉原則[OCP]:對拓展開放,對修改關閉)
在這種情況下基於子類的動態代理產生了。基於子類的動態代理不是由JDK內附功能。需要引入第三方的開發包:CGlib,使用這個子類的代理需要引入cglib-nodep.jar
CGLib在代碼上的使用基本和基於接口的動態代理一樣,使用方法極為相似,僅表現在CGLib代理時使用的是方法攔截器MethodInterceptor。代碼使用如下:
1 package com.scl.designpattern.proxy.dynamic.cglib; 2 3 //嬰兒類 4 public class Child 5 { 6 public void eat() 7 { 8 System.out.println("eat something...."); 9 } 10 11 public void run() 12 { 13 System.out.println("Child run very slow"); 14 } 15 16 protected void breath() 17 { 18 System.out.println("Child breath slowly"); 19 } 20 }
1 package com.scl.designpattern.proxy.dynamic.cglib; 2 3 import java.lang.reflect.Method; 4 5 import net.sf.cglib.proxy.Enhancer; 6 import net.sf.cglib.proxy.MethodInterceptor; 7 import net.sf.cglib.proxy.MethodProxy; 8 9 public class HealthHandle implements MethodInterceptor 10 { 11 12 private Object proxyTarget; 13 14 public Object getProxyInstance(Object target) 15 { 16 this.proxyTarget = target; 17 18 return Enhancer.create(target.getClass(), target.getClass().getInterfaces(), this); 19 } 20 21 @Override 22 public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable 23 { 24 Object methodResult = null; 25 if (method.getName().equals("eat")) 26 { 27 System.out.println("wash hand before eat"); 28 methodResult = method.invoke(proxyTarget, args); 29 methodProxy.invokeSuper(proxy, args); 30 System.out.println("wash hand after eat"); 31 } 32 else 33 { 34 methodResult = method.invoke(proxyTarget, args); 35 } 36 return methodResult; 37 } 38 }
1 package com.scl.designpattern.proxy.dynamic.cglib; 2 3 //客戶端 4 public class Client2 5 { 6 public static void main(String[] args) 7 { 8 HealthHandle h = new HealthHandle(); 9 Child child = (Child) h.getProxyInstance(new Child()); 10 child.eat(); 11 child.breath(); 12 } 13 }
源碼Interceptor簽名如下
1 /** 2 * All generated proxied methods call this method instead of the original method. 3 * The original method may either be invoked by normal reflection using the Method object, 4 * or by using the MethodProxy (faster). 官方建議使用MethodProxy對方法進行調用 5 * @param obj "this", the enhanced object 跟接口代理一樣,第一個參數為Enhanced對象實例 6 * @param method intercepted Method 7 * @param args argument array; primitive types are wrapped 8 * @param proxy used to invoke super (non-intercepted method); may be called 9 * as many times as needed 10 * @throws Throwable any exception may be thrown; if so, super method will not be invoked 11 * @return any value compatible with the signature of the proxied method. Method returning void will ignore this value. 12 * @see MethodProxy 13 */ 14 public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,MethodProxy proxy) throws Throwable;
另外要補充的兩點:
1. 方法攔截器對protected修飾的方法可以進行調用
2. 官方推薦在對委托方法進行調用時使用MethodProxy對方法進行調用。這樣有兩個好處:①官方說速度比較快 ②在intercept內調用委托類方法時不用保存委托對象引用
以上為本次對代理模式的總結,如有錯誤煩請指出糾正。轉載請注明出處。
