代理模式是開發中經常使用的一種設計模式,每一種設計模式的出現都會極大的解決某方面的問題,代理模式也是同樣,本文將會用通俗的語言來解釋什麼是代理模式?代理模式的種類、代碼示例、每種代理模式的優缺點和代理模式適用的場景。html
代理模式是什麼?
首先咱們用一個小故事來描述下什麼是代理模式,這會讓你更快的理解代理模式的相關角色,爲後面的各類代理打下基礎。java
假如,你是一個大明星,人氣很旺,粉絲也特別多。由於人氣高,因此不少商家想找你代言廣告,可是想要找你代言的人特別多,每一個商家你都須要進行商務洽談,若是聊得不錯決定合做,後續還須要簽署不少合同文件、記錄、備案等。這麼多商家找你代言,其中你只能選擇其中幾個代言,即使只選擇幾個,你也忙不過來。因而你就想了一個辦法,給本身找了一個經紀人,給經紀人制定標準讓他去對接各商家,經紀人作事很認真負責,不只剔除了不少不良的商家還對有資格的商家作了詳細的記錄,記錄商家的代言費、商家詳細信息、商家合同等信息。因而在商務代言這件事情上你只須要專心代言拍廣告,其餘的事情交由經紀人一併處理。面試
分析下整個事件,能夠知道,經紀人就是代理人,明星就是被代理人。在明星的廣告代言中,經紀人處理的商務洽談和簽約環節至關於代理,這就是代理模式在實際生活中的簡單案例。spring
其實不止經紀人和明星,生活中還有不少行爲本質就是代理模式,好比:某些大牌的飲料三級代理銷售、酒水的省市縣的代理人、三國時曹操挾天子以令諸侯等等。數據庫
說了這麼多案例,都是關於代理模式的,那既然這麼多人都在用代理模式,那代理模式必定解決了生活中的某些棘手的問題,那到底是什麼問題呢?編程
在明星和經紀人這個案例中,由於把代言這個商業行爲作了細分,讓明星團隊中每一個人負責代言的一部分,使每人只須要專一於本身的事,提升每一個人的專業度的同時,也提升了效率,這就叫專業,專人專事。設計模式
由於經紀人專一廣告代言的代理行爲,商業經驗豐富,因此經紀人也能夠用他的專業知識爲其餘明星作廣告代言的代理,這就叫能力複用。數組
那麼,如何使用代碼展現經紀人代理明星的廣告行爲呢?這其中有是如何運用代理模式的呢?安全
類比上面的明星和經紀人的例子:多線程
假若有個明星類,咱們想在調用明星類的代言方法以前作一些其餘操做好比權限控制、記錄等,那麼就須要一箇中間層,先執行中間層,在執行明星類的代言方法。
那講到這裏,想必又有人問,直接在明星類上加一個權限控制、記錄等方法不就好了麼,爲何非要用代理呢?
這就是本文最重要的一個核心知識,程序設計中的一個原則:類的單一性原則。這個原則很簡單,就是每一個類的功能儘量單一,在這個案例中讓明星類保持功能單一,就是對代理模式的通俗解釋。
那爲何要保持類的功能單一呢?
由於只有功能單一,這個類被改動的可能性纔會最小,其餘的操做交給其餘類去辦。在這個例子中,若是在明星類里加上權限控制功能,那麼明星類就再也不是單一的明星類了,是明星加經紀人二者功能的合併類。
若是咱們只想用權限控制功能,使用經紀人的功能給其餘明星篩選廣告商家,若是二者合併,就要建立這個合併類,可是咱們只使用權限功能,這就致使功能不單一,長期功能的累加會使得代碼極爲混亂,難以複用。
因此類的單一性原則和功能複用在代碼設計上很重要,這也是使用代理模式的核心。
而這整個過程所涉及到的角色能夠分爲四類:
- 主題接口:類比代言這類行爲的統稱,是定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法;
- 真實主題:類比明星這個角色,是真正實現業務邏輯的類;
- 代理類:類比經紀人這個角色,是用來代理和封裝真實主題;
- Main:類比商家這個角色,是客戶端,使用代理類和主題接口完成一些工做;
在java語言的發展中,出現了不少種代理方式,這些代理方式能夠分類爲兩類:靜態代理和動態代理,下面咱們就結合代碼實例解釋下,各種代理的幾種實現方式,其中的優缺點和適用的場景。
靜態代理
主題接口
package com.shuai.proxy; public interface IDBQuery { String request(); }
真實主題
package com.shuai.proxy.staticproxy; import com.shuai.proxy.IDBQuery; public class DBQuery implements IDBQuery { public DBQuery() { try { Thread.sleep(1000);//假設數據庫鏈接等耗時操做 } catch (InterruptedException ex) { ex.printStackTrace(); } } @Override public String request() { return "request string"; } }
代理類
package com.shuai.proxy.staticproxy; import com.shuai.proxy.IDBQuery; public class DBQueryProxy implements IDBQuery { private DBQuery real = null; @Override public String request() { // TODO Auto-generated method stub System.out.println("在此以前,記錄下什麼東西吧....."); //在真正須要的時候才能建立真實對象,建立過程可能很慢 if (real == null) { real = new DBQuery(); }//在多線程環境下,這裏返回一個虛假類,相似於 Future 模式 String result = real.request(); System.out.println("在此以後,記錄下什麼東西吧....."); return result; } }
Main客戶端
package com.shuai.proxy.staticproxy; import com.shuai.proxy.IDBQuery; public class Test { public static void main(String[] args) { IDBQuery q = new DBQueryProxy(); //使用代裏 q.request(); //在真正使用時才建立真實對象 } }
能夠看到,主題接口是IDBQuery,真實主題是DBQuery 實現了IDBQuery接口,代理類是DBQueryProxy,在代理類的方法裏實現了DBQuery類,而且在代碼裏寫死了代理先後的操做,這就是靜態代理的簡單實現,能夠看到靜態代理的實現優缺點十分明顯。
靜態代理的優缺點:
優勢:
使得真實主題處理的業務更加純粹,再也不去關注一些公共的事情,公共的業務由代理來完成,實現業務的分工,公共業務發生擴展時變得更加集中和方便。
缺點:
這種實現方式很直觀也很簡單,但其缺點是代理類必須提早寫好,若是主題接口發生了變化,代理類的代碼也要隨着變化,有着高昂的維護成本。
針對靜態代理的缺點,是否有一種方式彌補?可以不須要爲每個接口寫上一個代理方法,那就動態代理。
動態代理
動態代理,在java代碼裏動態代理類使用字節碼動態生成加載
技術,在運行時生成加載類。
生成動態代理類的方法不少,好比:JDK 自帶的動態處理、CGLIB、Javassist、ASM 庫。
- JDK 的動態代理使用簡單,它內置在 JDK 中,所以不須要引入第三方 Jar 包,但相對功能比較弱。
- CGLIB 和 Javassist 都是高級的字節碼生成庫,整體性能比 JDK 自帶的動態代理好,並且功能十分強大。
- ASM 是低級的字節碼生成工具,使用 ASM 已經近乎於在使用 Java bytecode 編程,對開發人員要求最高,固然,也是性能最好的一種動態代理生成工具。但 ASM 的使用很繁瑣,並且性能也沒有數量級的提高,與 CGLIB 等高級字節碼生成工具相比,ASM 程序的維護性較差,若是不是在對性能有苛刻要求的場合,仍是推薦 CGLIB 或者 Javassist。
這裏介紹兩種很是經常使用的動態代理技術,面試時也會經常用到的技術:JDK 自帶的動態處理
、CGLIB
兩種。
jDK動態代理
Java提供了一個Proxy類,使用Proxy類的newInstance方法能夠生成某個對象的代理對象,該方法須要三個參數:
-
類裝載器【通常咱們使用的是被代理類的裝載器】
-
指定接口【指定要被代理類的接口】
-
代理對象的方法裏幹什麼事【實現handler接口】
初次看見會有些不理解,不要緊,下面用一個實例來詳細展現JDK動態代理的實現:
代理類的實現
package com.shuai.proxy.jdkproxy; import com.shuai.proxy.staticproxy.DBQuery; import com.shuai.proxy.IDBQuery; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class DBQueryHandler implements InvocationHandler { private IDBQuery realQuery = null;//定義主題接口 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //若是第一次調用,生成真實主題 if (realQuery == null) { realQuery = new DBQuery(); } if ("request".equalsIgnoreCase(method.getName())) { System.out.println("調用前作點啥,助助興....."); Object result = method.invoke(realQuery, args); System.out.println("調用後作點啥,助助興....."); return result; } else { // 若是不是調用request方法,返回真實主題完成實際的操做 return method.invoke(realQuery, args); } } static IDBQuery createProxy() { IDBQuery proxy = (IDBQuery) Proxy.newProxyInstance( ClassLoader.getSystemClassLoader(), //當前類的類加載器 new Class[]{IDBQuery.class}, //被代理的主題接口 new DBQueryHandler() // 代理對象,這裏是當前的對象 ); return proxy; } }
Main客戶端
package com.shuai.proxy.jdkproxy; import com.shuai.proxy.IDBQuery; public class Test { // 客戶端測試方法 public static void main(String[] args) { IDBQuery idbQuery = DBQueryHandler.createProxy(); idbQuery.request(); } }
用debug的方式啓動,能夠看到方法被代理到代理類中實現,在代理類中執行真實主題的方法先後能夠進行不少操做。
雖然這種方法實現看起來很方便,可是細心的同窗應該也已經觀察到了,JDK動態代理技術的實現是必需要一個接口才行的,因此JDK動態代理的優缺點也很是明顯:
優勢:
- 不須要爲真實主題寫一個形式上徹底同樣的封裝類,減小維護成本;
- 能夠在運行時制定代理類的執行邏輯,提高系統的靈活性;
缺點:
- JDK動態代理,真實主題 必須實現的主題接口,若是真實主題 沒有實現主圖接口,或者沒有主題接口,則不能生成代理對象。
因爲必需要有接口才能使用JDK的動態代理,那是否有一種方式能夠沒有接口只有真實主題實現類也可使用動態代理呢?這就是第二種動態代理:CGLIB
;
CGLIB動態代理
使用 CGLIB
生成動態代理,首先須要生成 Enhancer
類實例,並指定用於處理代理業務的回調類。在 Enhancer.create()
方法中,會使用 DefaultGeneratorStrategy.Generate()
方法生成動態代理類的字節碼,並保存在 byte 數組中。接着使用 ReflectUtils.defineClass()
方法,經過反射,調用 ClassLoader.defineClass()
方法,將字節碼裝載到 ClassLoader 中,完成類的加載。最後使用 ReflectUtils.newInstance()
方法,經過反射,生成動態類的實例,並返回該實例。基本流程是根據指定的回調類生成 Class 字節碼—經過 defineClass()
將字節碼定義爲類—使用反射機制生成該類的實例。
真實主題
package com.shuai.proxy.cglibproxy; class BookImpl { void addBook() { System.out.println("增長圖書的普通方法..."); } }
代理類
package com.shuai.proxy.cglibproxy; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class BookImplProxyLib implements MethodInterceptor { /** * 建立代理對象 * * @return */ Object getBookProxyImplInstance() { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(BookImpl.class); // 回調方法 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; } }
Main客戶端
package com.shuai.proxy.cglibproxy; public class Test { public static void main(String[] args) { BookImplProxyLib cglib = new BookImplProxyLib(); BookImpl bookCglib = (BookImpl) cglib.getBookProxyImplInstance(); bookCglib.addBook(); } }
CGLIB的優缺點
優勢:
CGLIB經過繼承的方式進行代理、不管目標對象沒有沒實現接口均可以代理,彌補了JDK動態代理的缺陷。
缺點:
- CGLib建立的動態代理對象性能比JDK建立的動態代理對象的性能高很多,可是CGLib在建立代理對象時所花費的時間卻比JDK多得多,因此對於單例的對象,由於無需頻繁建立對象,用CGLib合適,反之,使用JDK方式要更爲合適一些。
- 因爲CGLib因爲是採用動態建立子類的方法,對於final方法,沒法進行代理。
代理模式的應用場合
代理模式有多種應用場合,以下所述:
- 遠程代理,也就是爲一個對象在不一樣的地址空間提供局部表明,這樣能夠隱藏一個對象存在於不一樣地址空間的事實。好比說 WebService,當咱們在應用程序的項目中加入一個 Web 引用,引用一個 WebService,此時會在項目中聲稱一個 WebReference 的文件夾和一些文件,這個就是起代理做用的,這樣可讓那個客戶端程序調用代理解決遠程訪問的問題;
- 虛擬代理,是根據須要建立開銷很大的對象,經過它來存放實例化須要很長時間的真實對象。這樣就能夠達到性能的最優化,好比打開一個網頁,這個網頁裏面包含了大量的文字和圖片,但咱們能夠很快看到文字,可是圖片倒是一張一張地下載後才能看到,那些未打開的圖片框,就是經過虛擬代裏來替換了真實的圖片,此時代理存儲了真實圖片的路徑和尺寸;
- 安全代理,用來控制真實對象訪問時的權限。通常用於對象應該有不一樣的訪問權限的時候;
- 指針引用,是指當調用真實的對象時,代理處理另一些事。好比計算真實對象的引用次數,這樣當該對象沒有引用時,能夠自動釋放它,或當第一次引用一個持久對象時,將它裝入內存,或是在訪問一個實際對象前,檢查是否已經釋放它,以確保其餘對象不能改變它。這些都是經過代理在訪問一個對象時附加一些內務處理;
- 延遲加載,用代理模式實現延遲加載的一個經典應用就在 Hibernate 框架裏面。當 Hibernate 加載實體 bean 時,並不會一次性將數據庫全部的數據都裝載。默認狀況下,它會採起延遲加載的機制,以提升系統的性能。Hibernate 中的延遲加載主要分爲屬性的延遲加載和關聯表的延時加載兩類。實現原理是使用代理攔截原有的 getter 方法,在真正使用對象數據時纔去數據庫或者其餘第三方組件加載實際的數據,從而提高系統性能。
參考: 代理模式原理及實例講解 爲何使用代理模式