代理模式是開發中常用的一種設計模式,每一種設計模式的出現都會極大的解決某方面的問題,代理模式也是一樣,本文將會用通俗的語言來解釋什么是代理模式?代理模式的種類、代碼示例、每種代理模式的優缺點和代理模式適用的場景。
代理模式是什么?
首先我們用一個小故事來描述下什么是代理模式,這會讓你更快的理解代理模式的相關角色,為后面的各種代理打下基礎。
假如,你是一個大明星,人氣很旺,粉絲也特別多。因為人氣高,所以很多商家想找你代言廣告,但是想要找你代言的人特別多,每個商家你都需要進行商務洽談,如果聊得不錯決定合作,后續還需要簽署很多合同文件、記錄、備案等。這么多商家找你代言,其中你只能選擇其中幾個代言,即便只選擇幾個,你也忙不過來。於是你就想了一個辦法,給自己找了一個經紀人,給經紀人制定標准讓他去對接各商家,經紀人做事很認真負責,不僅剔除了很多不良的商家還對有資格的商家做了詳細的記錄,記錄商家的代言費、商家詳細信息、商家合同等信息。於是在商務代言這件事情上你只需要專心代言拍廣告,其他的事情交由經紀人一並處理。
分析下整個事件,可以知道,經紀人就是代理人,明星就是被代理人。在明星的廣告代言中,經紀人處理的商務洽談和簽約環節相當於代理,這就是代理模式在實際生活中的簡單案例。
其實不止經紀人和明星,生活中還有很多行為本質就是代理模式,比如:某些大牌的飲料三級代理銷售、酒水的省市縣的代理人、三國時曹操挾天子以令諸侯等等。
說了這么多案例,都是關於代理模式的,那既然這么多人都在用代理模式,那代理模式一定解決了生活中的某些棘手的問題,那究竟是什么問題呢?
在明星和經紀人這個案例中,因為把代言這個商業行為做了細分,讓明星團隊中每個人負責代言的一部分,使每人只需要專注於自己的事,提高每個人的專業度的同時,也提高了效率,這就叫專業,專人專事。
因為經紀人專注廣告代言的代理行為,商業經驗豐富,所以經紀人也可以用他的專業知識為其他明星做廣告代言的代理,這就叫能力復用。
那么,如何使用代碼展示經紀人代理明星的廣告行為呢?這其中有是如何運用代理模式的呢?
類比上面的明星和經紀人的例子:
假如有個明星類,我們想在調用明星類的代言方法之前做一些其他操作比如權限控制、記錄等,那么就需要一個中間層,先執行中間層,在執行明星類的代言方法。
那講到這里,想必又有人問,直接在明星類上加一個權限控制、記錄等方法不就行了么,為什么非要用代理呢?
這就是本文最重要的一個核心知識,程序設計中的一個原則:類的單一性原則。這個原則很簡單,就是每個類的功能盡可能單一,在這個案例中讓明星類保持功能單一,就是對代理模式的通俗解釋。
那為什么要保持類的功能單一呢?
因為只有功能單一,這個類被改動的可能性才會最小,其他的操作交給其他類去辦。在這個例子中,如果在明星類里加上權限控制功能,那么明星類就不再是單一的明星類了,是明星加經紀人兩者功能的合並類。
如果我們只想用權限控制功能,使用經紀人的功能給其他明星篩選廣告商家,如果兩者合並,就要創建這個合並類,但是我們只使用權限功能,這就導致功能不單一,長期功能的累加會使得代碼極為混亂,難以復用。
所以類的單一性原則和功能復用在代碼設計上很重要,這也是使用代理模式的核心。
而這整個過程所涉及到的角色可以分為四類:
- 主題接口:類比代言這類行為的統稱,是定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法;
- 真實主題:類比明星這個角色,是真正實現業務邏輯的類;
- 代理類:類比經紀人這個角色,是用來代理和封裝真實主題;
- 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 方法,在真正使用對象數據時才去數據庫或者其他第三方組件加載實際的數據,從而提升系統性能。
參考:
代理模式原理及實例講解
為什么使用代理模式