代理模式
代理模式的定義很簡單:給某一對象提供一個代理對象,並由代理對象控制對原對象的引用。
代理模式的結構
有些情況下,一個客戶不想活着不能夠直接引用一個對象,可以通過代理對象在客戶端和目標對象之間起到中介作用。代理模式中的角色有:
1、抽象對象角色
聲明瞭目標對象和代理對象的共同接口,這樣一來在任何可以使用目標對象的地方都可以使用代理對象
2、目標對象角色
定義了代理對象所代表的目標對象
3、代理對象角色
代理對象內部含有目標對象的引用,從而可以在任何時候操作目標對象;代理對象提供一個與目標對象相同的接口,以便可以在任何時候替代目標對象
靜態代理示例
假如一個接口裏面有一個方法,想在調用這個接口的前後都加一點東西,就可以使用代理模式。靜態代理是代理模式最簡單的實現,先定義一個靜態代理接口,裏面有一個print()方法,看一下:
public interface StaticHelloWorld { // 定義一個接口,裏面有一個打印方法 void print(); }
讓一個子類實現它,打印一句"Hello World"出來:
public class StaticHelloWorldImpl implements StaticHelloWorld { public void print() { System.out.println("Hello World"); } }
給這個接口創建一個代理對象,來實現對接口實現類的代理。注意,這裏的重點是代理對象和實際對象實現的是同一個接口,因爲希望在任何時候讓代理對象替代實際對象:
public class StaticProxy implements StaticHelloWorld { private StaticHelloWorld staticHelloWorld; public StaticProxy(StaticHelloWorld staticHelloWorldImpl) { this.staticHelloWorld = staticHelloWorldImpl; } public void print() { System.out.println("Before Hello World!"); staticHelloWorld.print(); System.out.println("After Hello World!"); } }
寫一個類去調用代理對象,在代理對象的構造函數中傳入一個實際對象即可:
public class StaticTestMain { public static void main(String[] args) { StaticHelloWorld shw = new StaticHelloWorldImpl(); StaticProxy sp = new StaticProxy(shw); sp.print(); } }
運行結果爲:
Before Hello World!
Hello World
After Hello World!
這個很明顯,就不說了。
靜態代理的缺點
靜態代理的特點是靜態代理的代理類是程序員創建的,在程序運行之前靜態代理的.class文件已經存在了。
從靜態代理來看,看到靜態代理模式確實可以有一個代理對象來控制實際對象的引用,並通過代理對象來使用實際對象。這種模式在代理量較小的時候還可以,但是代理量一大起來,就存在着三個比較大的缺點:
1、如果想換一種代理內容,比如我在"Hello World"前後不想輸入"Before XXX"和"After XXX"了,想輸出運行前後系統當前時間,就必須新寫一個代理對象。這樣很容易造成代理對象的膨脹。
2、代理內容無法複用,也就是說"Before XXX"和"After XXX"只可以給某一個類使用,另一個類如果也想使用這個代理內容,必須自己也寫一個,同樣,造成的後果就是代理類的無限膨脹
3、接口裏面如果新增了一個方法,實際對象實現了這個方法,代理對象也必須新增內容,去給這個新增方法增加代理內容(假如需要的話)
利用JDK中的代理類Proxy實現動態代理的示例
由於靜態代理的侷限性,所以產生了動態代理的概念。看一下,首先還是定義一個動態代理接口:
public interface DynamicHelloWorld { // 動態代理類,有一個print()方法 String print(); }
寫一個類去實現它,打印方法打印"Enter DynamicHelloWorldImpl.print()"並返回"DynamicHelloWorldImpl":
public class DynamicHelloWorldImpl implements DynamicHelloWorld { public String print() { System.out.println("Enter DynamicHelloWorldImpl.print()"); return "DynamicHelloWorldImpl"; } }
最關鍵的一部分,動態代理類,也是不太好理解的一部分。在Java中,動態代理需要實現InvocationHandler接口。
InvocationHandler接口裏面只有一個方法invoke(),至於如何實現,完全看使用者自己的喜好,沒有固定。可以像下面這樣,把newInstance即生成一個動態代理類的過程放到InvocationHandler的實現類中:
public class DynamicProxy implements InvocationHandler { private Object target; public Object newInstance(Object target) { this.target = target; return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Before DynamicProxy"); method.invoke(target, args); System.out.println("After DynamicProxy"); return null; } }
也可以像下面這樣,讓動態代理類在外面生成,只在構造函數中傳入一個target:
public class DynamicProxy implements InvocationHandler { private Object target; public DynamicProxy(Object target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Before DynamicProxy"); method.invoke(target, args); System.out.println("After DynamicProxy"); return null; } }
如果是前者的寫法,那麼main函數要這麼寫:
public class DynamicTestMain { public static void main(String[] args) throws Exception { DynamicProxy dp = new DynamicProxy(); DynamicHelloWorld dhwi = new DynamicHelloWorldImpl(); DynamicHelloWorld dhw = (DynamicHelloWorld)dp.newInstance(dhwi); dhw.print(); } }
如果是後者的寫法,那麼main函數要這麼寫:
public class DynamicTestMain { public static void main(String[] args) throws Exception { DynamicHelloWorld dhwi = new DynamicHelloWorldImpl(); InvocationHandler ih = new DynamicProxy(dhwi); DynamicHelloWorld dhw = (DynamicHelloWorld)Proxy. newProxyInstance(DynamicHelloWorld.class.getClassLoader(), new Class<?>[]{DynamicHelloWorld.class}, ih); dhw.print(); } }
不管哪種寫法,運行結果都是一樣的:
Before DynamicProxy
Enter DynamicHelloWorldImpl.print()
After DynamicProxy
動態代理解析
上面兩種寫法,本質上都是一樣的。萬變不離其宗,歸納起來,實現一個動態代理可以總結爲如下四步:
1、獲取要被代理的對象,也就是實際對象
2、實現InvocationHandler接口,生成實際的代理內容
3、利用Proxy.newInstance()方法生成一個代理內容,第三個參數傳入InvocationHandler的實現類
4、代理對象調用接口內部的方法
動態代理,利用動態編譯+反射技術,把對實際對象的方法調用轉換成對傳入的InvocationHandler接口實現類的invoke方法的調用,這是動態代理模式實現的關鍵點。要證明這一點並不難,在程序中打上斷點跟一下代碼即可:
按F5執行下一步,看到程序走到了InvocationHandler實現類的invoke方法中:
執行完methd.invoke(target, args),看到控制檯打印出了"Enter DynamicHelloWorldImpl.print()"這一句語句,說明執行了實際對象DynamicHelloWorldImpl的print()方法:
這證明了我們的結論,把對於實際對象方法的調用轉換爲對於InvocationHandler實現類的invoke方法的調用
動態代理的優點
1、最直觀的,類少了很多
2、代理內容也就是InvocationHandler接口的實現類可以複用,可以給A接口用、也可以給B接口用,A接口用了InvocationHandler接口實現類A的代理,不想用了,可以方便地換成InvocationHandler接口實現B的代理
3、最重要的,用了動態代理,就可以在不修改原來代碼的基礎上,就在原來代碼的基礎上做操作,這就是AOP即面向切面編程
動態代理的缺點
動態代理有一個最大的缺點,就是它只能針對接口生成代理,不能只針對某一個類生成代理,比方說我們在調用Proxy的newProxyInstance方法的時候,第二個參數傳某個具體類的getClass(),那麼會報錯:
Exception in thread "main" java.lang.IllegalArgumentException: proxy.DynamicHelloWorldImpl is not an interface
這是因爲java.lang.reflect.Proxy的newProxyInstance方法會判斷傳入的Class是不是一個接口:
... /* * Verify that the Class object actually represents an * interface. */ if (!interfaceClass.isInterface()) { throw new IllegalArgumentException( interfaceClass.getName() + " is not an interface"); } ...
而實際使用中,我們爲某一個單獨的類實現一個代理也很正常,這種情況下,我們就可以考慮使用CGLIB(一種字節碼增強技術)來爲某一個類實現代理了。