java代理模式原理及例項講解
東漢末年,大將軍何進引董卓入京,想借西北王的軍隊對抗閹黨,無奈自己先被閹黨做掉,而後造成鉅變,導致諸侯並起,最終形成三國鼎立局面。漢獻帝即位後,初平三年(公元 192 年),治中從事毛玠向曹操建議“奉天子以令不臣”,曹操採納了他的建議,迎接漢獻帝來到許昌。漢獻帝劉協在許都沒有實際的權利,曹操不斷地誅除公卿大臣,不斷地集軍政大權於一身。建安元年八月,曹操進駐洛陽,立刻趁張楊、楊奉兵眾在外,趕跑了韓暹,接著做了三件事:殺侍中臺崇、尚書馮碩等,謂“討有罪”;封董承、伏完等,謂“賞有功”;追賜射聲校尉沮俊,謂“矜死節”。然後在第九天趁他人尚未來得及反應的情況下,遷帝都許,使皇帝擺脫其他勢力的控制。此後,他還加緊步伐剪除異己,提高自己的權勢。他首先向最有影響力的三公發難,罷免太尉楊彪、司空張喜;其次誅殺議郎趙彥;再次是發兵征討楊奉,解除近兵之憂;最後是一方面以天子名義譴責袁紹,打擊其氣焰,另一方面將大將軍讓予袁紹,穩定大敵。這就是歷史上著名的“挾天子以令諸侯”。漢獻帝與曹操的關係,是歷史上兩位偉大的政治家的聯手,穩定了東漢政權,最終平穩交接給曹魏政權,也間接映射了我們本文要講解的“代理模式”。
代理模式
代理模式使用代理物件完成使用者請求,遮蔽使用者對真實物件的訪問。現實世界的代理人被授權執行當事人的一些事宜,無需當事人出面,從第三方的角度看,似乎當事人並不存在,因為他只和代理人通訊。而事實上代理人是要有當事人的授權,並且在核心問題上還需要請示當事人。
在軟體設計中,使用代理模式的意圖也很多,比如因為安全原因需要遮蔽客戶端直接訪問真實物件,或者在遠端呼叫中需要使用代理類處理遠端方法呼叫的技術細節 (如 RMI),也可能為了提升系統性能,對真實物件進行封裝,從而達到延遲載入的目的。
代理模式角色分為 4 種:
主題介面:定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法;
真實主題:真正實現業務邏輯的類;
代理類:用來代理和封裝真實主題;
Main:客戶端,使用代理類和主題介面完成一些工作。
延遲載入
以一個簡單的示例來闡述使用代理模式實現延遲載入的方法及其意義。假設某客戶端軟體有根據使用者請求去資料庫查詢資料的功能。在查詢資料前,需要獲得資料庫連線,軟體開啟時初始化系統的所有類,此時嘗試獲得資料庫連線。當系統有大量的類似操作存在時 (比如 XML 解析等),所有這些初始化操作的疊加會使得系統的啟動速度變得非常緩慢。為此,使用代理模式的代理類封裝對資料庫查詢中的初始化操作,當系統啟動時,初始化這個代理類,而非真實的資料庫查詢類,而代理類什麼都沒有做。因此,它的構造是相當迅速的。
在系統啟動時,將消耗資源最多的方法都使用代理模式分離,可以加快系統的啟動速度,減少使用者的等待時間。而在使用者真正做查詢操作時再由代理類單獨去載入真實的資料庫查詢類,完成使用者的請求。這個過程就是使用代理模式實現了延遲載入。
延遲載入的核心思想是:如果當前並沒有使用這個元件,則不需要真正地初始化它,使用一個代理物件替代它的原有的位置,只要在真正需要的時候才對它進行載入。使用代理模式的延遲載入是非常有意義的,首先,它可以在時間軸上分散系統壓力,尤其在系統啟動時,不必完成所有的初始化工作,從而加速啟動時間;其次,對很多真實主題而言,在軟體啟動直到被關閉的整個過程中,可能根本不會被呼叫,初始化這些資料無疑是一種資源浪費。例如使用代理類封裝資料庫查詢類後,系統的啟動過程這個例子。若系統不使用代理模式,則在啟動時就要初始化 DBQuery 物件,而使用代理模式後,啟動時只需要初始化一個輕量級的物件 DBQueryProxy。
下面程式碼 IDBQuery 是主題介面,定義代理類和真實類需要對外提供的服務,定義了實現資料庫查詢的公共方法 request() 函式。DBQuery 是真實主題,負責實際的業務操作,DBQueryProxy 是 DBQuery 的代理類。
清單 1. 延遲載入代理
12345678910111213141516171819202122232425262728293031323334353637383940 | public interface IDBQuery { String request(); } public class DBQuery implements IDBQuery{ public DBQuery(){ try{ Thread.sleep(1000);//假設資料庫連線等耗時操作 }catch(InterruptedException ex){ ex.printStackTrace(); } } @Override public String request() { // TODO Auto-generated method stub return "request string"; } } public class DBQueryProxy implements IDBQuery{ private DBQuery real = null; @Override public String request() { // TODO Auto-generated method stub //在真正需要的時候才能建立真實物件,建立過程可能很慢 if(real==null){ real = new DBQuery(); }//在多執行緒環境下,這裡返回一個虛假類,類似於 Future 模式 return real.request(); } } public class Main { public static void main(String[] args){ IDBQuery q = new DBQueryProxy(); //使用代裡 q.request(); //在真正使用時才建立真實物件 } } |
動態代理
動態代理是指在執行時動態生成代理類。即,代理類的位元組碼將在執行時生成並載入當前代理的 ClassLoader。與靜態處理類相比,動態類有諸多好處。首先,不需要為真實主題寫一個形式上完全一樣的封裝類,假如主題介面中的方法很多,為每一個介面寫一個代理方法也很麻煩。如果介面有變動,則真實主題和代理類都要修改,不利於系統維護;其次,使用一些動態代理的生成方法甚至可以在執行時制定代理類的執行邏輯,從而大大提升系統的靈活性。
動態代理類使用位元組碼動態生成載入技術,在執行時生成載入類。生成動態代理類的方法很多,如,JDK 自帶的動態處理、CGLIB、Javassist 或者 ASM 庫。JDK 的動態代理使用簡單,它內建在 JDK 中,因此不需要引入第三方 Jar 包,但相對功能比較弱。CGLIB 和 Javassist 都是高階的位元組碼生成庫,總體效能比 JDK 自帶的動態代理好,而且功能十分強大。ASM 是低階的位元組碼生成工具,使用 ASM 已經近乎於在使用 Java bytecode 程式設計,對開發人員要求最高,當然,也是效能最好的一種動態代理生成工具。但 ASM 的使用很繁瑣,而且效能也沒有數量級的提升,與 CGLIB 等高階位元組碼生成工具相比,ASM 程式的維護性較差,如果不是在對效能有苛刻要求的場合,還是推薦 CGLIB 或者 Javassist。
以清單 1 所示程式碼中的 DBQueryProxy 為例,使用動態代理生成動態類,替換上例中的 DBQueryProxy。首先,使用 JDK 的動態代理生成代理物件。JDK 的動態代理需要實現一個處理方法呼叫的 Handler,用於實現代理方法的內部邏輯。
清單 2. 動態代理
1234567891011121314151617181920 | import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class DBQueryHandler implements InvocationHandler{ IDBQuery realQuery = null;//定義主題介面 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // TODO Auto-generated method stub //如果第一次呼叫,生成真實主題 if(realQuery == null){ realQuery = new DBQuery(); } //返回真實主題完成實際的操作 return realQuery.request(); } } |
以上程式碼實現了一個 Handler,可以看到,它的內部邏輯和 DBQueryProxy 是類似的。在呼叫真實主題的方法前,先嚐試生成真實主題物件。接著,需要使用這個 Handler 生成動態代理物件。程式碼如清單 3 所示。
清單 3. 生成動態代理物件
12345678910111213141516171819202122232425262728 | import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class DBQueryHandler implements InvocationHandler{ IDBQuery realQuery = null;//定義主題介面 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // TODO Auto-generated method stub //如果第一次呼叫,生成真實主題 if(realQuery == null){ realQuery = new DBQuery(); } //返回真實主題完成實際的操作 return realQuery.request(); } public static IDBQuery createProxy(){ IDBQuery proxy = (IDBQuery)Proxy.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class[]{IDBQuery.class}, new DBQueryHandler() ); return proxy; } } |
以上程式碼生成了一個實現了 IDBQuery 介面的代理類,代理類的內部邏輯由 DBQueryHandler 決定。生成代理類後,由 newProxyInstance() 方法返回該代理類的一個例項。至此,一個完整的動態代理完成了。
在 Java 中,動態代理類的生成主要涉及對 ClassLoader 的使用。以 CGLIB 為例,使用 CGLIB 生成動態代理,首先需要生成 Enhancer 類例項,並指定用於處理代理業務的回撥類。在 Enhancer.create() 方法中,會使用 DefaultGeneratorStrategy.Generate() 方法生成動態代理類的位元組碼,並儲存在 byte 陣列中。接著使用 ReflectUtils.defineClass() 方法,通過反射,呼叫 ClassLoader.defineClass() 方法,將位元組碼裝載到 ClassLoader 中,完成類的載入。最後使用 ReflectUtils.newInstance() 方法,通過反射,生成動態類的例項,並返回該例項。基本流程是根據指定的回撥類生成 Class 位元組碼—通過 defineClass() 將位元組碼定義為類—使用反射機制生成該類的例項。從清單 4 到清單 7 所示是使用 CGLIB 動態反射生成類的完整過程。
清單 4. 定義介面
123 | public interface BookProxy { public void addBook(); } |
清單 5. 定義實現類
123456 | //該類並沒有申明 BookProxy 介面 public class BookProxyImpl { public void addBook() { System.out.println("增加圖書的普通方法..."); } } |
清單 6. 定義反射類及過載方法
12345678910111213141516171819202122232425262728293031323334 | import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; public class BookProxyLib implements MethodInterceptor { private Object target; /** * 建立代理物件 * * @param target * @return */ public Object getInstance(Object target) { this.target = target; Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(this.target.getClass()); // 回撥方法 enhancer.setCallback(this); // 建立代理物件 return enhancer.create(); } @Override // 回撥方法 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("事物開始"); proxy.invokeSuper(obj, args); System.out.println("事物結束"); return null; } } |
清單 7. 執行程式
1234567 | public class TestCglib { public static void main(String[] args) { BookProxyLib cglib=new BookProxyLib(); BookProxyImpl bookCglib=(BookProxyImpl)cglib.getInstance(new BookProxyImpl()); bookCglib.addBook(); } } |
清單 8. 執行輸出
123 | 事物開始 增加圖書的普通方法... 事物結束 |
代理模式的應用場合
代理模式有多種應用場合,如下所述:
遠端代理,也就是為一個物件在不同的地址空間提供區域性代表,這樣可以隱藏一個物件存在於不同地址空間的事實。比如說 WebService,當我們在應用程式的專案中加入一個 Web 引用,引用一個 WebService,此時會在專案中聲稱一個 WebReference 的資料夾和一些檔案,這個就是起代理作用的,這樣可以讓那個客戶端程式呼叫代理解決遠端訪問的問題;
虛擬代理,是根據需要建立開銷很大的物件,通過它來存放例項化需要很長時間的真實物件。這樣就可以達到效能的最優化,比如開啟一個網頁,這個網頁裡面包含了大量的文字和圖片,但我們可以很快看到文字,但是圖片卻是一張一張地下載後才能看到,那些未開啟的圖片框,就是通過虛擬代裡來替換了真實的圖片,此時代理儲存了真實圖片的路徑和尺寸;
安全代理,用來控制真實物件訪問時的許可權。一般用於物件應該有不同的訪問許可權的時候;
指標引用,是指當呼叫真實的物件時,代理處理另外一些事。比如計算真實物件的引用次數,這樣當該物件沒有引用時,可以自動釋放它,或當第一次引用一個持久物件時,將它裝入記憶體,或是在訪問一個實際物件前,檢查是否已經釋放它,以確保其他物件不能改變它。這些都是通過代理在訪問一個物件時附加一些內務處理;
延遲載入,用代理模式實現延遲載入的一個經典應用就在 Hibernate 框架裡面。當 Hibernate 載入實體 bean 時,並不會一次性將資料庫所有的資料都裝載。預設情況下,它會採取延遲載入的機制,以提高系統的效能。Hibernate 中的延遲載入主要分為屬性的延遲載入和關聯表的延時載入兩類。實現原理是使用代理攔截原有的 getter 方法,在真正使用物件資料時才去資料庫或者其他第三方元件載入實際的資料,從而提升系統性能。
結束語
設計模式是前人工作的總結和提煉。通常,被人們廣泛流傳的設計模式都是對某一特定問題的成熟的解決方案。如果能合理地使用設計模式,不僅能使系統更容易地被他人理解,同時也能使系統擁有更加合理的結構。本文對代理模式的 4 種角色、延遲載入、動態代理等做了一些介紹,希望能夠幫助讀者對代理模式有進一步的瞭解。