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