一、概述
??代理模式我們接觸的就比較多了,所謂的代理模式就是,給某一個對象提供一個代理對象,并由代理對象控制對原對象的引用。比如,在一些情況下,一個客戶不想或者不能夠直接引用一個對象,而代理對象可以在客戶端和目標(biāo)對象之前起到中介的作用。其實代理模式就兩個字:中介。
- 代理模式主要解決的問題:
直接訪問對象相關(guān)的問題,比如無法直接訪問對象,或者由于安全問題不建議直接訪問的對象,亦或者是訪問對象太過于復(fù)雜的問題;
- 為什么使用代理:
由于設(shè)計模式的開閉原則,不建議直接修改已有的代碼,我們可以使用代理,在目標(biāo)對象基礎(chǔ)上合理的擴展功能;
- 代理模式的一些應(yīng)用場景:
- 比如我最近要買ThinkPadT470p,國內(nèi)網(wǎng)站上全是9千元左右,這時我可以通過海淘從國外買,可能會便宜不少,這其中的海淘就相當(dāng)于代理的角色。
- 只要我們隨便一想,生活中涉及到代理的實在太多,比如火車站通過黃牛買票,通過4S店買車,通過中介租房,外賣小哥送餐等等,都是代理模式的應(yīng)用。
- 代理模式的分類:
根據(jù)代理對象生成的時期不同,代理模式可以大致分為靜態(tài)代理和動態(tài)代理。其中JDK中的動態(tài)代理的實現(xiàn)是通過Java的反射來實現(xiàn)的。
代理模式角色
我們先來一張圖看一下代理模式的角色:
注:圖片來源:圖說設(shè)計模式-代理模式-角色圖解
通過以上結(jié)構(gòu),我們可以大概了解到代理模式的幾個角色:
- Subject:抽象主題角色,底層一般是接口實現(xiàn),該接口定義了代理類和真實主題角色的公共的對外方法,是對象和它的代理共用的接口。
- RealSubject:真實主題角色,實現(xiàn)了抽象主題接口,實現(xiàn)了真正的業(yè)務(wù)邏輯;
- Proxy:代理角色,內(nèi)部含有對真實對象RealSubject的引用,從而可以實現(xiàn)對真實對象的代理。代理對象提供與真實對象相同的接口,以便在任何時刻都能代替真實對象。同時,代理對象可以在執(zhí)行真實對象操作時,附加其他的操作,以擴展相應(yīng)的功能。
- Client:客戶端的調(diào)用,不算代理模式的角色。
二、靜態(tài)代理
靜態(tài)代理是說由程序員手動編寫或工具生成的代理類,在程序運行前就已經(jīng)編譯完成,這就是所謂的靜態(tài)代理類。我們通過一個簡單的保存數(shù)據(jù)的例子來看一下靜態(tài)代理的實現(xiàn),代碼轉(zhuǎn)載自:
Java三種代理模式:靜態(tài)代理、動態(tài)代理和cglib代理,靜態(tài)代理代碼實現(xiàn)
- 底層接口:
IUserDao
,只有一個保存數(shù)據(jù)的接口。
public interface IUserDao {
void save();
}
- 真實對象:
UserDaoImpl
,實現(xiàn)了接口中的save方法
public class UserDaoImpl implements IUserDao {
@Override
public void save() {
System.out.println("保存數(shù)據(jù)");
}
}
- 靜態(tài)代理對象:
UserDaoProxy
,也實現(xiàn)了接口IUserDao
;
public class UserDaoProxy implements IUserDao {
private IUserDao iUserDao;
public UserDaoProxy(IUserDao iUserDao) {
this.iUserDao = iUserDao;
}
@Override
public void save() {
iUserDao.save();
}
}
- 測試類:
Main
public class Main {
public static void main(String[] args) {
//目標(biāo)對象
IUserDao target = new UserDaoImpl();
//代理對象
UserDaoProxy proxy = new UserDaoProxy(target);
proxy.save();
}
}
運行測試代碼后,打印:
保存數(shù)據(jù)
這時候,如果要擴展功能,比如說,對原先的保存接口添加事務(wù)處理。當(dāng)然,我們可以直接修改具體實現(xiàn)類UserDaoImpl
,但根據(jù)設(shè)計模式的開閉原則,不建議我們直接修改已實現(xiàn)的類,這個時候我們就可以通過代理來實現(xiàn),我們通過給UserDaoProxy
添加對應(yīng)的方法即可:
public class UserDaoProxy implements IUserDao {
private IUserDao iUserDao;
public UserDaoProxy(IUserDao iUserDao) {
this.iUserDao = iUserDao;
}
@Override
public void save() {
before();
iUserDao.save();
after();
}
private void before() {
System.out.println("開啟事務(wù)");
}
private void after() {
System.out.println("關(guān)閉事務(wù)");
}
}
重新運行程序:
開啟事務(wù)
保存數(shù)據(jù)
關(guān)閉事務(wù)
靜態(tài)代理總結(jié)
- 靜態(tài)代理可以做到在不修改目標(biāo)實現(xiàn)的情況下,對目標(biāo)功能進行擴展;并且代理對象作為客戶端和目標(biāo)對象之間的中介,起到了保護目標(biāo)對象的作用;
- 一般來說,靜態(tài)代理具體實現(xiàn)類與代理類要一一對應(yīng),并且代理對象需要與目標(biāo)對象實現(xiàn)一樣的接口,所以有可能會有很多代理類。并且,一旦接口增加方法,目標(biāo)對象與代理對象都需要維護,無形之中增加了系統(tǒng)的復(fù)雜度;
三、動態(tài)代理
??相比靜態(tài)代理,動態(tài)代理有更強的靈活性。動態(tài)代理是在程序運行時,通過反射機制動態(tài)創(chuàng)建生成的。動態(tài)代理類使用字節(jié)碼動態(tài)生成加載技術(shù),在運行時生成加載類。生成動態(tài)代理類的方法很多,如JDK自帶的動態(tài)代理、CGLIB、Javassist 或者 ASM 庫等。我們先來看下JDK自帶的生成動態(tài)代理的方式。
JDK動態(tài)代理
1. 實現(xiàn)類和方法
我們首先來看下JDK動態(tài)代理中涉及到的基礎(chǔ)類和方法:
類或接口:
java.lang.reflect.Proxy
,java.lang.reflect InvocationHandler
,位于Java反射包下;
方法:Proxy
中的newProxyInstance
方法,InvocationHandler
中的invoke
方法;
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) {...}
該方法返回一個目標(biāo)接口的代理類實例。參數(shù)簡單說明:
- ClassLoader loader:指定當(dāng)前目標(biāo)對象使用的類加載器
- Class<?>[] interfaces:目標(biāo)對象實現(xiàn)的接口的類型
- InvocationHandler h:用于事件處理,執(zhí)行目標(biāo)對象的方法時,會觸發(fā)事件處理器的方法,會把當(dāng)前執(zhí)行目標(biāo)對象的方法作為參數(shù)傳入
public Object invoke(Object proxy, Method method, Object[] args)
這個方法用于實現(xiàn)目標(biāo)對象具體方法的調(diào)用。
2. 實現(xiàn)步驟
JDK代理的大致實現(xiàn)步驟如下:
a. 創(chuàng)建底層接口及真實的對象;
b. 創(chuàng)建一個InvocationHandler接口的實現(xiàn)類,實現(xiàn)invoke()方法;
c. 調(diào)用Proxy的靜態(tài)方法,創(chuàng)建一個代理類
d. 通過代理調(diào)用方法
3. 代碼實現(xiàn)
接下來,我們來看一下代碼實現(xiàn),我們先新建代理類:
public class DynamicProxy implements InvocationHandler {
/** 要代理的對象 */
private Object object;
/**
* 將被代理者的實例傳進動態(tài)代理類的構(gòu)造函數(shù)中
* @param object
*/
public DynamicProxy(Object object) {
this.object = object;
}
/**
* 覆蓋InvocationHandler接口的invoke方法,代理實現(xiàn)具體方法的調(diào)用,并可以添加我們的實現(xiàn)
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(this.object, args);
after();
return result;
}
/**
* 獲取代理對象
*
* @return the instance
*/
public Object getInstance() {
return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), this);
}
private void before() {
System.out.println("開啟事務(wù)");
}
private void after() {
System.out.println("關(guān)閉事務(wù)");
}
}
測試程序:
public static void main(String[] args) {
//目標(biāo)對象,并且打印下
IUserDao target = new UserDaoImpl();
System.out.println(target.getClass());
// 代理對象,并且打印下
IUserDao userDao = (IUserDao)new DynamicProxy(target).getInstance();
System.out.println(userDao.getClass());
userDao.save();
}
打印結(jié)果:
class com.proxy.UserDaoImpl
class com.sun.proxy.$Proxy0
開啟事務(wù)
保存數(shù)據(jù)
關(guān)閉事務(wù)
4.JDK代理總結(jié)
- JDK動態(tài)代理與靜態(tài)代理相比較,最大的好處是接口中聲明的所有方法都被轉(zhuǎn)移到一個集中的方法中處理,即使接口方法數(shù)量比較多的時候,我們也可以進行靈活的處理;
- JDK動態(tài)代理對象不需要實現(xiàn)接口,但是要求目標(biāo)對象必須實現(xiàn)接口,否則不能使用動態(tài)代理。而CGLIB動態(tài)代理則是彌補了這部分的不足。
CGLIB動態(tài)代理
1. CGLIB簡介
??我們上面所了解的靜態(tài)代理和JDK動態(tài)代理模式,都是要求目標(biāo)對象是實現(xiàn)一個接口的類,但是有時候目標(biāo)對象只是一個單獨的對象,并沒有實現(xiàn)任何的接口,這時候就可以考慮通過CGLIB代理來實現(xiàn)了。
??CGLIB(Code Generation Library),是一個第三方代碼生成庫,它的原理是程序運行的時候,為指定的目標(biāo)類生成一個子類,從而實現(xiàn)對目標(biāo)對象功能的擴展。所以有時候CGLIB代理也可以叫做子類代理。由于是通過繼承來實現(xiàn)的,所以我們不能對
final
修飾的類進行代理。
CGLIB有以下特點:
- Cglib是一個強大的高性能的代碼生成包,它可以在運行期擴展java類與實現(xiàn)java接口.它廣泛的被許多AOP的框架使用,例如Spring AOP和synaop,為他們提供方法的interception(攔截);
- CGLIB包的底層是通過使用一個小而快的字節(jié)碼處理框架ASM,來轉(zhuǎn)換字節(jié)碼并生成新的類。不鼓勵直接使用ASM,因為它需要你對JVM內(nèi)部結(jié)構(gòu)包括class文件的格式和指令集都很熟悉。
2. CGLIB代碼實現(xiàn)
由于CGLIB是一個第三方包,所以我們在使用的時候需要手動引入cglib的jar,并且由于CGLIB底層使用了ASM字節(jié)碼開源包,所以還需要引入ASM的jar包。對應(yīng)的maven引入是:
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.ow2.asm/asm -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>5.2</version>
</dependency>
其實,CGLIB的實現(xiàn)和JDK動態(tài)代理的實現(xiàn)是類似的,我們簡單來看一下代理類:
public class CGLIBProxy implements MethodInterceptor {
/** 代理的對象 */
private Object object;
public CGLIBProxy(Object object) {
this.object = object;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
before();
Object result = method.invoke(object, args);
after();
return result;
}
/**
* 獲取代理對象
*
* @return the instance
*/
public Object getInstance() {
//工具類
Enhancer en = new Enhancer();
//設(shè)置父類
en.setSuperclass(object.getClass());
//設(shè)置回調(diào)函數(shù)
en.setCallback(this);
//創(chuàng)建子類對象代理
return en.create();
}
private void before() {
System.out.println("開啟事務(wù)");
}
private void after() {
System.out.println("關(guān)閉事務(wù)");
}
}
進行測試:
public static void main(String[] args) {
// 實際對象 并打印下
UserDaoImpl userDao = new UserDaoImpl();
System.out.println(userDao.getClass());
// 代理對象 并打印下
UserDaoImpl userProxy = (UserDaoImpl)new CGLIBProxy(userDao).getInstance();
System.out.println(userProxy.getClass());
userProxy.save();
}
打印結(jié)果:
class com.proxy.UserDaoImpl
class com.proxy.UserDaoImpl$$EnhancerByCGLIB$$ceeeb256
開啟事務(wù)
保存數(shù)據(jù)
關(guān)閉事務(wù)
3. CGLIB總結(jié)
- CGLIB與JDK動態(tài)代理的最大區(qū)別就是要代理的對象是否實現(xiàn)了接口;
- CGLIB是基于繼承來實現(xiàn)的動態(tài)代理,所以要求被代理的類不能是
final
類型;- Spring的AOP 是JDK動態(tài)代理和CGLIB代理的很好的一個實現(xiàn)。如果對象實現(xiàn)了接口,則默認情況下AOP會采用JDK動態(tài)代理,也可以強制使用CGLIB代理;如果目標(biāo)對象沒有實現(xiàn)接口,則AOP會采用CGLIB代理,也就是說Spring會自動在JDK動態(tài)代理和CGLIB之間進行選擇;
其他動態(tài)代理
Javassist代理
- Javassist, 也是一個開源的Java字節(jié)碼的類庫,是和CGLIB類似,屬于一種高級的字節(jié)碼生成庫,通過Javassist對字節(jié)碼操作來實現(xiàn)代理,比如使用Javassist對JBoss動態(tài)的實現(xiàn)AOP框架。如果有需要,我們可以看下相應(yīng)的實現(xiàn)。
ASM代理
- ASM,同樣也是一種字節(jié)碼生成庫,ASM能夠以二進制形式修改已有類或者動態(tài)生成類,ASM從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。
- 不過相對于Javassist來說,ASM屬于一種相對低級的字節(jié)碼庫,在創(chuàng)建class字節(jié)碼的過程中,操縱的級別是底層JVM的匯編指令級別,這要求ASM使用者要對class組織結(jié)構(gòu)和JVM匯編指令有一定的了解。雖然ASM是性能最高的一種動態(tài)代理方式,但由于操作繁瑣,要求較高,所以一般情況下,如果不是在對性能有苛刻要求的場合,還是推薦 CGLIB 或者 Javassist。
四、 應(yīng)用場景
以下摘錄自:IBM-代理模式的應(yīng)用場合
代理模式有多種應(yīng)用場合,如下所述:
- 遠程代理,也就是為一個對象在不同的地址空間提供局部代表,這樣可以隱藏一個對象存在于不同地址空間的事實。比如說 WebService,當(dāng)我們在應(yīng)用程序的項目中加入一個 Web 引用,引用一個 WebService,此時會在項目中聲稱一個 WebReference 的文件夾和一些文件,這個就是起代理作用的,這樣可以讓那個客戶端程序調(diào)用代理解決遠程訪問的問題;
- 虛擬代理,是根據(jù)需要創(chuàng)建開銷很大的對象,通過它來存放實例化需要很長時間的真實對象。這樣就可以達到性能的最優(yōu)化,比如打開一個網(wǎng)頁,這個網(wǎng)頁里面包含了大量的文字和圖片,但我們可以很快看到文字,但是圖片卻是一張一張地下載后才能看到,那些未打開的圖片框,就是通過虛擬代里來替換了真實的圖片,此時代理存儲了真實圖片的路徑和尺寸;
- 安全代理,用來控制真實對象訪問時的權(quán)限。一般用于對象應(yīng)該有不同的訪問權(quán)限的時候;
- 指針引用,是指當(dāng)調(diào)用真實的對象時,代理處理另外一些事。比如計算真實對象的引用次數(shù),這樣當(dāng)該對象沒有引用時,可以自動釋放它,或當(dāng)?shù)谝淮我靡粋€持久對象時,將它裝入內(nèi)存,或是在訪問一個實際對象前,檢查是否已經(jīng)釋放它,以確保其他對象不能改變它。這些都是通過代理在訪問一個對象時附加一些內(nèi)務(wù)處理;
- 延遲加載,用代理模式實現(xiàn)延遲加載的一個經(jīng)典應(yīng)用就在 Hibernate 框架里面。當(dāng) Hibernate 加載實體 bean 時,并不會一次性將數(shù)據(jù)庫所有的數(shù)據(jù)都裝載。默認情況下,它會采取延遲加載的機制,以提高系統(tǒng)的性能。Hibernate 中的延遲加載主要分為屬性的延遲加載和關(guān)聯(lián)表的延時加載兩類。實現(xiàn)原理是使用代理攔截原有的 getter 方法,在真正使用對象數(shù)據(jù)時才去數(shù)據(jù)庫或者其他第三方組件加載實際的數(shù)據(jù),從而提升系統(tǒng)性能。
五、回顧與總結(jié)
在以上內(nèi)容中,我們學(xué)習(xí)了代理模式的兩種情況:靜態(tài)代理和動態(tài)代理,并且學(xué)習(xí)了動態(tài)代理中JDK代理和CGLIB代理的實現(xiàn)。我們簡單總結(jié)下:
- 所謂代理,其實就是為了解決直接訪問對象相關(guān)的問題,其實數(shù)白了就兩個字:中介;
- 靜態(tài)代理和動態(tài)代理其實類似,都需要生成代理類,只是生成代理類的時期不同,靜態(tài)代理是在編譯期就已生成,而動態(tài)代理是在程序運行期間動態(tài)生成的;
- JDK代理和CGLIB代理最大的區(qū)別就是要代理的對象是否實現(xiàn)了接口;他們兩者不是相互對立的局面,而是相互協(xié)作的,而Spring中的AOP正是他們協(xié)作的最佳實踐;
- 了解與合理的使用設(shè)計模式,不但能讓我們能容易的理解別人優(yōu)秀的代碼,也可以讓我們寫出優(yōu)秀的代碼;
參考資料:
《大話設(shè)計模式》
Java三種代理模式:靜態(tài)代理、動態(tài)代理和cglib代理
Java設(shè)計模式——代理模式實現(xiàn)及原理
代理模式原理及實例講解