設(shè)計模式-代理模式(四)

一、概述

??代理模式我們接觸的就比較多了,所謂的代理模式就是,給某一個對象提供一個代理對象,并由代理對象控制對原對象的引用。比如,在一些情況下,一個客戶不想或者不能夠直接引用一個對象,而代理對象可以在客戶端和目標(biāo)對象之前起到中介的作用。其實代理模式就兩個字:中介

  1. 代理模式主要解決的問題:

直接訪問對象相關(guān)的問題,比如無法直接訪問對象,或者由于安全問題不建議直接訪問的對象,亦或者是訪問對象太過于復(fù)雜的問題;

  1. 為什么使用代理:

由于設(shè)計模式的開閉原則,不建議直接修改已有的代碼,我們可以使用代理,在目標(biāo)對象基礎(chǔ)上合理的擴展功能;

  1. 代理模式的一些應(yīng)用場景:
  1. 比如我最近要買ThinkPadT470p,國內(nèi)網(wǎng)站上全是9千元左右,這時我可以通過海淘從國外買,可能會便宜不少,這其中的海淘就相當(dāng)于代理的角色。
  2. 只要我們隨便一想,生活中涉及到代理的實在太多,比如火車站通過黃牛買票,通過4S店買車,通過中介租房,外賣小哥送餐等等,都是代理模式的應(yīng)用。
  1. 代理模式的分類:

根據(jù)代理對象生成的時期不同,代理模式可以大致分為靜態(tài)代理和動態(tài)代理。其中JDK中的動態(tài)代理的實現(xiàn)是通過Java的反射來實現(xiàn)的。

代理模式角色

我們先來一張圖看一下代理模式的角色:

Proxy.jpg

注:圖片來源:圖說設(shè)計模式-代理模式-角色圖解
通過以上結(jié)構(gòu),我們可以大概了解到代理模式的幾個角色:

  1. Subject:抽象主題角色,底層一般是接口實現(xiàn),該接口定義了代理類和真實主題角色的公共的對外方法,是對象和它的代理共用的接口。
  2. RealSubject:真實主題角色,實現(xiàn)了抽象主題接口,實現(xiàn)了真正的業(yè)務(wù)邏輯;
  3. Proxy:代理角色,內(nèi)部含有對真實對象RealSubject的引用,從而可以實現(xiàn)對真實對象的代理。代理對象提供與真實對象相同的接口,以便在任何時刻都能代替真實對象。同時,代理對象可以在執(zhí)行真實對象操作時,附加其他的操作,以擴展相應(yīng)的功能。
  4. 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)

  1. 底層接口:IUserDao,只有一個保存數(shù)據(jù)的接口。
public interface IUserDao {
    void save();
}
  1. 真實對象:UserDaoImpl,實現(xiàn)了接口中的save方法
public class UserDaoImpl implements IUserDao {
    @Override
    public void save() {
        System.out.println("保存數(shù)據(jù)");
    }
}
  1. 靜態(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();
    }
}
  1. 測試類: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é)
  1. 靜態(tài)代理可以做到在不修改目標(biāo)實現(xiàn)的情況下,對目標(biāo)功能進行擴展;并且代理對象作為客戶端和目標(biāo)對象之間的中介,起到了保護目標(biāo)對象的作用;
  2. 一般來說,靜態(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.Proxyjava.lang.reflect InvocationHandler,位于Java反射包下;
方法:Proxy中的newProxyInstance方法,InvocationHandler中的invoke方法;

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h) {...}

該方法返回一個目標(biāo)接口的代理類實例。參數(shù)簡單說明:

  1. ClassLoader loader:指定當(dāng)前目標(biāo)對象使用的類加載器
  2. Class<?>[] interfaces:目標(biāo)對象實現(xiàn)的接口的類型
  3. 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é)
  1. JDK動態(tài)代理與靜態(tài)代理相比較,最大的好處是接口中聲明的所有方法都被轉(zhuǎn)移到一個集中的方法中處理,即使接口方法數(shù)量比較多的時候,我們也可以進行靈活的處理;
  2. 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é)
  1. CGLIB與JDK動態(tài)代理的最大區(qū)別就是要代理的對象是否實現(xiàn)了接口;
  2. CGLIB是基于繼承來實現(xiàn)的動態(tài)代理,所以要求被代理的類不能是final類型;
  3. 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代理
  1. Javassist, 也是一個開源的Java字節(jié)碼的類庫,是和CGLIB類似,屬于一種高級的字節(jié)碼生成庫,通過Javassist對字節(jié)碼操作來實現(xiàn)代理,比如使用Javassist對JBoss動態(tài)的實現(xiàn)AOP框架。如果有需要,我們可以看下相應(yīng)的實現(xiàn)。
ASM代理
  1. ASM,同樣也是一種字節(jié)碼生成庫,ASM能夠以二進制形式修改已有類或者動態(tài)生成類,ASM從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。
  2. 不過相對于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)用場合,如下所述:

  1. 遠程代理,也就是為一個對象在不同的地址空間提供局部代表,這樣可以隱藏一個對象存在于不同地址空間的事實。比如說 WebService,當(dāng)我們在應(yīng)用程序的項目中加入一個 Web 引用,引用一個 WebService,此時會在項目中聲稱一個 WebReference 的文件夾和一些文件,這個就是起代理作用的,這樣可以讓那個客戶端程序調(diào)用代理解決遠程訪問的問題;
  2. 虛擬代理,是根據(jù)需要創(chuàng)建開銷很大的對象,通過它來存放實例化需要很長時間的真實對象。這樣就可以達到性能的最優(yōu)化,比如打開一個網(wǎng)頁,這個網(wǎng)頁里面包含了大量的文字和圖片,但我們可以很快看到文字,但是圖片卻是一張一張地下載后才能看到,那些未打開的圖片框,就是通過虛擬代里來替換了真實的圖片,此時代理存儲了真實圖片的路徑和尺寸;
  3. 安全代理,用來控制真實對象訪問時的權(quán)限。一般用于對象應(yīng)該有不同的訪問權(quán)限的時候;
  4. 指針引用,是指當(dāng)調(diào)用真實的對象時,代理處理另外一些事。比如計算真實對象的引用次數(shù),這樣當(dāng)該對象沒有引用時,可以自動釋放它,或當(dāng)?shù)谝淮我靡粋€持久對象時,將它裝入內(nèi)存,或是在訪問一個實際對象前,檢查是否已經(jīng)釋放它,以確保其他對象不能改變它。這些都是通過代理在訪問一個對象時附加一些內(nèi)務(wù)處理;
  5. 延遲加載,用代理模式實現(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é)下:

  1. 所謂代理,其實就是為了解決直接訪問對象相關(guān)的問題,其實數(shù)白了就兩個字:中介;
  2. 靜態(tài)代理和動態(tài)代理其實類似,都需要生成代理類,只是生成代理類的時期不同,靜態(tài)代理是在編譯期就已生成,而動態(tài)代理是在程序運行期間動態(tài)生成的;
  3. JDK代理和CGLIB代理最大的區(qū)別就是要代理的對象是否實現(xiàn)了接口;他們兩者不是相互對立的局面,而是相互協(xié)作的,而Spring中的AOP正是他們協(xié)作的最佳實踐;
  4. 了解與合理的使用設(shè)計模式,不但能讓我們能容易的理解別人優(yōu)秀的代碼,也可以讓我們寫出優(yōu)秀的代碼;

參考資料:
《大話設(shè)計模式》
Java三種代理模式:靜態(tài)代理、動態(tài)代理和cglib代理
Java設(shè)計模式——代理模式實現(xiàn)及原理
代理模式原理及實例講解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內(nèi)容