Java靜態代理與動態代理模式的實現
前言: 在現實生活中,考慮以下的場景:小王打算要去租房,他相中了一個房子,準備去找房東洽談相關事宜。但是房東他很忙,平時上班沒時間,總沒有時間見面,他也沒辦法。後來,房東想了一個辦法,他找到了一個人代替自己和小王洽談,房東本人不用出面,他只要把他的對房客的要求告訴他找的那個人,那個人和你商量就可以了,這樣就可以完成租房這件事了。這種現實場景比比皆是,所呈現出來的其實就是代理模式的原型的一種。我們把焦點轉向編程,你是否在編程中經常遇見這樣一個問題,對於訪問某個對象,我們希望給它的方法前加入一個標記,比如對象的方法開始執行、結束等等(比如日誌記錄)。怎麽辦呢,這個時候只要我們編寫一個復制的類,然後把這個對象傳給這個類,再對這個類進行操作,不就可以了嗎。這就是代理模式,復制的類就是代理對象,通過代理對象與我們進行打交道就可以對它原來的對象進行改造。對於有些時候現有的對象不能滿足我們的需求的時候,如何對它進行擴展,對方法進行改造,使其適用於我們所面臨的問題,這就是代理模式的思維出發點。
本篇博客的目錄:
一:代理模式的介紹
二:實現靜態代理
三:代理的進階:實現動態代理
四:總結
接下來按照目錄,我們來依次講解本篇博客:
一:代理模式的介紹
1.1:目標
為其他對象提供一種代理以控制對這個對象的訪問
解釋:在實際編程中我們會產生一個代理對象,然後去引用被代理對象,對被代理對象進行控制與訪問,實現客戶端對原代理對象的訪問,詳情見下面的代碼示例。
1.2:適用性
在需要用比較通用和復雜的對象指針代替簡單的指針的時候,使用Proxy 模式。下面是一 些可以使用Proxy 模式常見情況:
1.2.1:遠程代理(Remote Proxy )為一個對象在不同的地址空間提供局部代表。 NEXTSTEP[Add94] 使用N X P r o x y
類實現了這一目的。
1.2.2:虛代理(Virtual
Proxy )根據需要創建開銷很大的對象。在動機一節描述的I m a g e P r o x y 就是這樣一種代理的例子。
1.2.3: 保護代理(Protection Proxy )控制對原始對象的訪問。保護代理用於對象應該有不同 的訪問權限的時候
1.2.4: 智能指引(Smart
Reference )取代了簡單的指針,它在訪問對象時執行一些附加操作。
1.3:結構
二:實現靜態代理
2.1:代碼場景
假如我們現在由以下的場景:文件編輯器要對一個圖像文件進行操作,遵循以下順序:加載,繪制,獲取長度和寬度,存儲四個步驟,但是有個問題,需要被加載的圖片非常大,每次加載的時候都要耗費很多時間。並且我們希望對圖片的操作可以記錄出來操作的步驟,比如第一步、第二步這樣便於我們去理解。為了解決這個問題,我們可以先考慮解決第一個問題就是利用代理模式去新建一個代理對象,然後在代理對象裏去實現一個緩存,這樣下次我們直接可以去緩存裏面取對象,而不用去新建,這樣就省去了新建對象消耗的資源。另一方面,我們可以考慮去引用原來的方法,再給這方法基礎上添加我們所要做的記錄。接下裏我們用java代碼來實現這個場景:
2.2:代碼示範
2.2.1:首先新建一個接口,命名為Graphic,其中主要規範了我們進行操作的步驟
public interface Graphic { void load();//加載 void Draw();//繪制 Extent GetExtent();//獲取長度和寬度 void Store();//存儲 }
2.2.2:然後去新建一個Image類,用於實現接口,對操作進行具體控制,註意為了其中的Extent是對寬度和長度的封裝(省略get和set方法)
public class Image implements Graphic{ public Image() { try { Thread.sleep(2000); //模擬創建需要花費很久的時間 System.out.println("正在創建對象"); } catch (Exception e) { e.printStackTrace(); } } @Override public void load() { System.out.println("進行加載.."); } @Override public void Draw() { System.out.println("進行繪畫.."); } @Override public Extent GetExtent() { Extent extent = new Extent("100","200"); System.out.println("獲取圖片的屬性是:"+extent.toString()); return extent; } @Override public void Store() { System.out.println("圖片進行存儲在硬盤裏.."); } }
public class Extent { private String width; private String length; public Extent(String width, String length) { super(); this.width = width; this.length = length; } //getter And setter方法 }
2.2.3:接下來就是很關鍵的一步了,新建我們的代理類,我們新建一個類叫做ImageProxy,然後實現緩存與記錄的效果:
import java.util.HashMap; import java.util.Map; public class ImageProxy implements Graphic{ private Image image; private Map<String , Image> cache = new HashMap<String, Image>();//緩存 public ImageProxy() { init(); } public void init(){ //只需要初始化一次 if (image==null) { image= new Image(); cache.put("image", image);//放入緩存 } } @Override public void load() { System.out.println("---第一步開始---"); image = cache.get("image"); image.load(); System.out.println("---第一步結束---"); } @Override public void Draw() { System.out.println("---第二步開始---"); image.Draw(); System.out.println("---第二步結束---"); } @Override public Extent GetExtent() { System.out.println("---第三步開始---"); Extent extent = image.GetExtent(); System.out.println("---第三步結束--"); return extent; } @Override public void Store() { System.out.println("---第四步開始---"); image.Store(); System.out.println("---第四步結束--"); } }
2.2.4:我們的文檔編輯器現在要開始進行文檔編輯了,我們來實現具體的代碼,我們先來引用一下原對象,看一下原來的對象會出現什麽情況:
public class DocumentEditor { public static void main(String[] args) { Graphic proxy = new Image();//引用代碼 proxy.load(); proxy.Draw(); proxy.GetExtent(); proxy.Store(); } }
2.2.5:測試代碼
正在創建對象 進行加載.. 進行繪畫.. 獲取圖片的屬性是:Extent [width=100, length=200] 圖片進行存儲在硬盤裏..
我們可以看出,它會消耗3秒才會出來具體的對象,並且沒有我們所需要的記錄。好了,我們把2.2.4的引用代碼改為: Graphic proxy = new ImageProxy();
我們再來測試一下:
正在創建對象 ---第一步開始--- 進行加載.. ---第一步結束--- ---第二步開始--- 進行繪畫.. ---第二步結束--- ---第三步開始--- 獲取圖片的屬性是:Extent [width=100, length=200] ---第三步結束-- ---第四步開始--- 圖片進行存儲在硬盤裏.. ---第四步結束--
很明顯可以看出,通過訪問我們的代理對象,就可以實現對原方法的改造,這就是代理模式的精髓思想。不過到這裏你可能會問,為什麽不對原對象進行改造呢?為什麽要給他新建一個代理對象,這不是很麻煩嗎。回答這個問題,首先要提一個代碼的設計原則,也就是有名的開閉原則:對擴展開放,對修改關閉。這句話的意思就是不建議對原有的代碼進行修改,我們要做的事就是盡量不用動原有的類和對象,在它的基礎上去改造,而不是直接去修改它。至於這個原則為什麽這樣,我想其中一個原因就是因為軟件體系中牽一發很動全身的事情很常見,很可能你修改了這一小塊,然而與此相關的很多東西就會發生變化。所以輕易不要修改,而是擴展。
三:實現動態代理
3.1:靜態代理的不足:
通過看靜態代理可以動態擴展我們的對象,但是有個問題,在我們進行方法擴展的時候,比如我們的日誌功能:每個前面都得寫第一步、第二步。如果我們要再一些其他的東西,比如權限校驗、代碼說明,一個兩個方法還好,萬一方法成百個呢,那我們豈不是要累死。這就是動態代理要解決的問題,只需要寫一次就可以,究竟是怎麽實現的呢,接下裏我們來一探究竟吧。
3.2:動態代理的準備:
動態代理需要用到JDk的Proxy類,通過它的newProxyInstance()方法可以生成一個代理類,我們來通過jdk看一下具體的說明,如何使用它:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
- 返回一個指定接口的代理類實例,該接口可以將方法調用指派到指定的調用處理程序。此方法相當於:
Proxy.getProxyClass(loader, interfaces). getConstructor(new Class[] { InvocationHandler.class }). newInstance(new Object[] { handler });
Proxy.newProxyInstance
拋出IllegalArgumentException
,原因與Proxy.getProxyClass
相同。 - 參數:
loader
- 定義代理類的類加載器interfaces
- 代理類要實現的接口列表h
- 指派方法調用的調用處理程序- 返回:
- 一個帶有代理類的指定調用處理程序的代理實例,它由指定的類加載器定義,並實現指定的接口
- 拋出:
IllegalArgumentException
- 如果違反傳遞到getProxyClass
的參數上的任何限制NullPointerException
- 如果interfaces
數組參數或其任何元素為null
,或如果調用處理程序h
為null
從中可以看出它有三個參數,分別是classlcoder、interface、InvocationHandler.只要我們把這三個參數傳遞給他,它就可以 返回給我們一個代理對象,訪問這個代理對象就可以實現對原對象的擴展。接下來,我們用代碼來實現它。
3.3:代碼場景
我們來做這樣一個場景,我們實現一個計算器,計算器裏面有加減乘除方法,然後我們實現這個計算的接口,有具體的類和被代理的類,我們通過動態代理來生成代理類,而不用自己去建了,好了,看接下來的代碼:
3.4:動態代理的代碼實現
3.4.1:首先我們新建一個接口,命名為Calculator ,聲明四個方法:
public interface Calculator { int add(int i,int j);//加 int sub(int i,int j);//減 int mul(int i,int j);//乘 double div(int i,int j);//除 }
3.4.2:新建一個實現類,命名為CalculatorImpl ,也就是被代理類
public class CalculatorImpl implements Calculator{ @Override public int add(int i, int j) { return i+j; } @Override public int sub(int i, int j) { return i-j; } @Override public int mul(int i, int j) { return i*j; } @Override public double div(int i, int j) { return (i/j); } }
3.4.3:新建一個類,命名為CalCulatorDynamicProxy,也就是我們的代理類,用來對上面的類進行代理:
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; public class CalCulatorDynamicProxy { //動態代理類 private Calculator calculator;//要代理的對象 public CalCulatorDynamicProxy(Calculator calculator) { this.calculator = calculator; } public Calculator getCalculator() { Calculator proxy = null; ClassLoader loader =calculator.getClass().getClassLoader();//獲取類加載器 Class[] interfaces = new Class[]{Calculator.class};//代理對象的類型 InvocationHandler h = new InvocationHandler() {//調用處理器 //proxy:正在返回的代理對象 //method:被調用的方法 //args:傳入的參數 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("---日誌記錄開始---"); String name = method.getName();//獲取方法的名字 System.out.println("方法"+name+"()開始執行了"); System.out.println("方法中的參數是:"+Arrays.asList(args)); Object result = method.invoke(calculator, args); System.out.println("方法執行後的結果是"+result); return result; } }; proxy=(Calculator)Proxy.newProxyInstance(loader, interfaces, h);//代理對象 return proxy; } }
這裏要特別強調的問題就是:invoke()方法,註意其中的參數,分別是被代理對象、方法、和對象參數,這裏的原理是反射,通過獲取原對象的class對象,然後進行處理,我們可以通過method對象拿到被代理對象的方法,也是add()、mul()、sub()、div()方法,也可以通過args對象數組取得傳入的參數,比如我們具體傳入的數值,再通過method.invoke()方法進行調用,就進行了被代理對象的方法的執行,然後就是返回的結果(如果方法前為void,返回的就是null)
3.4.4:我們來做具體的測試
public class Test { public static void main(String[] args) { Calculator cal = new CalculatorImpl(); Calculator proxy = new CalCulatorDynamicProxy(cal).getCalculator(); int add = proxy.add(29, 1); int sub = proxy.sub(9, 2); int mul = proxy.mul(3, 7); double div = proxy.div(6,8); } }
具體的測試結果:
---日誌記錄開始--- 方法add()開始執行了 方法中的參數是:[29, 1] 方法執行後的結果是30 ---日誌記錄開始--- 方法sub()開始執行了 方法中的參數是:[9, 2] 方法執行後的結果是7 ---日誌記錄開始--- 方法mul()開始執行了 方法中的參數是:[3, 7] 方法執行後的結果是21 ---日誌記錄開始--- 方法div()開始執行了 方法中的參數是:[6, 8] 方法執行後的結果是0.0
可以看出動態代理模式輕松完成了對被代理對象的日誌記錄功能,並且只用寫一次,這樣即便有成百上千的方法我們也不怕,這就是動態代理領先於靜態代理之處,雖然實現起來有點麻煩,但是其方便,動態的給被代理對象添加功能。我們所寫的重復代碼更少,做的事情更少。
四:總結
本篇博客介紹了動態代理和靜態代理的概念,並對其進行了代碼實現,在實際的工作中,我們會經常遇到需要代理模式的地方,希望能多多思考,促進我們形成一定的思維模式。並且動態代理作為SpringAop的實現原理,封裝了動態代理,讓我們實現起來更加方便,對於這部分內容可以只做了解,理解其背後的運行機制即可,並不需要具體實現,如果需要實現,直接使用spring的Aop功能即可。
希望看完本篇博客,能對代理這種思維有深入的理解。好了,本篇博文就講到這裏,謝謝。
Java靜態代理與動態代理模式的實現