Java中幾種常見的設計模式--代理設計模式
一、什麼是代理模式
代理模式是一種設計模式,是在不修改原始碼下,實現目標物件的功能擴充套件。
舉個例子,有個歌手物件Singer,然後這個物件有個方法sing()。
public class Singer { public void sing(){ System.out.println("演唱一首歌曲"); } }
假如你希望,通過你的某種方式生產出來的歌手物件,在唱歌前後還要想觀眾問好和答謝,也即對目標物件Singer的sing方法進行功能擴充套件。
public void sing(){ System.out.println("向觀眾們問好"); System.out.println("演唱一首歌曲"); System.out.println("歌手答謝"); } }
但是往往你又不能直接對原始碼進行修改,可能是你希望原來的物件還保持原來的樣子,又或許你提供的只是一個可插拔的外掛,甚至你有可能都不知道你要對哪個目標物件進行擴充套件。這時就需要用到java的代理模式了。
代理模式有三種,靜態代理模式、動態代理模式、Cglib代理模式,下面我們分別用這三種模式來實現我們想要的功能。
二、靜態代理模式
在使用靜態代理模式時,我們要首先定義介面或父類,讓代理物件和被代理物件一起實現相同的介面或相同的父類。我們建立的介面如下:
/** * 代理類和被代理類共同介面*/ public interface ISinger { void sing(); }
/** * 介面實現 * 目標物件 */ public class Singer implements ISinger{ @Override public void sing(){ System.out.println("演唱一首歌曲"); } }
/** * 代理物件和物件物件實現相同的介面 * */ public class SingerProxy implements ISinger { private ISinger singer;//注入目標物件,以便實現sing()方法 public SingerProxy(ISinger singer){ this.singer=singer; } //對sing()方法進行拓展 @Override public void sing() { System.out.println("向觀眾們問好"); singer.sing(); System.out.println("歌手答謝"); } }
測試程式碼如下:
@Test public void sing() { //建立目標物件 ISinger singer=new Singer(); //建立代理物件 ISinger proxySinger=new SingerProxy(singer); //執行代理拓展的方法 proxySinger.sing(); }
結果如下:
向觀眾們問好
演唱一首歌曲
歌手答謝
這種靜態代理模式的優缺點
優點:簡單直觀。
缺點:代理程式碼必須提前寫出,需要對代理程式碼進行維護。如果介面層發生了變化,也需要修改代理程式碼。
如果能在執行時動態地寫出代理物件,不但減少了一大批代理類的程式碼,也少了不斷維護的煩惱,不過執行時的效率必定受到影響。這種方式就是接下來的動態代理。
三、動態代理模式
動態代理模式有以下特點
- 代理物件,不需要實現介面
- 代理物件的生成,是利用JDK的API,動態的在記憶體中構建代理物件(需要我們指定建立代理物件/目標物件實現的介面的型別)
- 動態代理也叫做:JDK代理,介面代理
代理類所在包:java.lang.reflect.Proxy
JDK實現代理只需要使用newProxyInstance方法,但是該方法需要接收三個引數,完整的寫法是:
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )
- ClassLoader loader:指定目標物件的類載入器,獲取類載入器的方法固定;
- Class<?>[] interfaces:指定目標物件的介面型別,使用泛型的方式獲取型別;
- InvocationHandler h:事件處理介面,執行目標物件方法時會觸發事件處理器的方法,會把當前執行目標物件的方法作為引數傳入。
我們可以將建立一個代理工廠類
public class ProxyFactory { //建立一個目標物件 Object target; public ProxyFactory(Object target){ this.target=target; } //給目標物件生成代理物件 public Object getProxyInstance(){ return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("向觀眾們問好"); Object returnResult=method.invoke(target,args); System.out.println("歌手答謝"); return returnResult; } }); } }
測試方法如下:
@Test public void TestProxy(){ //建立目標物件 ISinger singer=new Singer(); System.out.println(singer.getClass()); //建立代理物件 ISinger proxySinger=(ISinger) new ProxyFactory(singer).getProxyInstance(); System.out.println(proxySinger.getClass()); //執行代理方法 proxySinger.sing(); }
結果如下:
class com.zc.core.Singer
class com.sun.proxy.$Proxy4
向觀眾們問好
演唱一首歌曲
歌手答謝
總結:以上程式碼只有標黃的部分是需要自己寫出,其餘部分全都是固定程式碼。由於java封裝了newProxyInstance這個方法的實現細節,所以使用起來才能這麼方便,具體的底層原理將會在下一小節說明。
缺點:可以看出靜態代理和JDK代理有一個共同的缺點,就是目標物件必須實現一個或多個介面,加入沒有,則可以使用Cglib代理。
四、Cglib代理模式
Cglib是一個強大的高效能的程式碼生成包,它可以在執行期擴充套件java類與實現java介面.它廣泛的被許多AOP的框架使用,例如Spring AOP和synaop,為他們提供方法的interception(攔截)。Cglib包的底層是通過使用一個小而塊的位元組碼處理框架ASM來轉換位元組碼並生成新的類.不鼓勵直接使用ASM,因為它要求你必須對JVM內部結構包括class檔案的格式和指令集都很熟悉。
使用Cglib的前提條件:
- 需要引入cglib的jar檔案,由於Spring的核心包中已經包括了Cglib功能,所以也可以直接引入spring-core-3.2.5.jar
- 引入功能包後,就可以在記憶體中動態構建子類
- 代理的類不能為final,否則報錯
- 目標物件的方法如果為final/static,那麼就不會被攔截,即不會執行目標物件額外的業務方法。
具體操作如下:
/** * 目標物件未實現任何介面 */ public class Singer{ public void sing(){ System.out.println("演唱一首歌曲"); } }
/** *Cglib子類代理工廠 */ public class ProxyFactory implements MethodInterceptor { //建立目標物件 private Object target; public ProxyFactory(Object target){ this.target=target; } //給目標物件建立一個代理物件 public Object getProxyInstance(){ //建立一個工具類 Enhancer enhancer=new Enhancer(); //設定父類 enhancer.setSuperclass(target.getClass()); //設定回撥函式 enhancer.setCallback(this); //建立子類(代理物件) return enhancer.create(); } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("向觀眾們問好"); //執行目標物件方法 Object returnResult=method.invoke(target,objects); System.out.println("歌手答謝"); return returnResult; } }
在代理工廠中只有標黃的地方是需要我們修改的,其他部分都是不變的。
測試方法如下:
public class Test { public static void main(String[] args){ //建立目標物件 Singer singer=new Singer(); //建立代理物件 Singer proxySinger=(Singer) new ProxyFactory(singer).getProxyInstance(); //執行代理方法 proxySinger.sing(); } }
測試結果:
向觀眾們問好
演唱一首歌曲
歌手答謝
總結:三種代理模式各有優缺點和相應的適用範圍,主要看目標物件是否實現了介面。以Spring框架所選擇的代理模式舉例
在Spring的AOP程式設計中:
如果加入容器的目標物件有實現介面,用JDK代理
如果目標物件沒有實現介面,用Cglib代理