代理模式的種類、原理及各種實(shí)例詳解

代理模式是開發(fā)中常用的一種設(shè)計(jì)模式,每一種設(shè)計(jì)模式的出現(xiàn)都會(huì)極大的解決某方面的問題,代理模式也是一樣,本文將會(huì)用通俗的語言來解釋什么是代理模式?代理模式的種類、代碼示例、每種代理模式的優(yōu)缺點(diǎn)和代理模式適用的場(chǎng)景。

代理模式是什么?

首先我們用一個(gè)小故事來描述下什么是代理模式,這會(huì)讓你更快的理解代理模式的相關(guān)角色,為后面的各種代理打下基礎(chǔ)。

假如,你是一個(gè)大明星,人氣很旺,粉絲也特別多。因?yàn)槿藲飧撸院芏嗌碳蚁胝夷愦詮V告,但是想要找你代言的人特別多,每個(gè)商家你都需要進(jìn)行商務(wù)洽談,如果聊得不錯(cuò)決定合作,后續(xù)還需要簽署很多合同文件、記錄、備案等。這么多商家找你代言,其中你只能選擇其中幾個(gè)代言,即便只選擇幾個(gè),你也忙不過來。于是你就想了一個(gè)辦法,給自己找了一個(gè)經(jīng)紀(jì)人,給經(jīng)紀(jì)人制定標(biāo)準(zhǔn)讓他去對(duì)接各商家,經(jīng)紀(jì)人做事很認(rèn)真負(fù)責(zé),不僅剔除了很多不良的商家還對(duì)有資格的商家做了詳細(xì)的記錄,記錄商家的代言費(fèi)、商家詳細(xì)信息、商家合同等信息。于是在商務(wù)代言這件事情上你只需要專心代言拍廣告,其他的事情交由經(jīng)紀(jì)人一并處理。

分析下整個(gè)事件,可以知道,經(jīng)紀(jì)人就是代理人,明星就是被代理人。在明星的廣告代言中,經(jīng)紀(jì)人處理的商務(wù)洽談和簽約環(huán)節(jié)相當(dāng)于代理,這就是代理模式在實(shí)際生活中的簡(jiǎn)單案例。

其實(shí)不止經(jīng)紀(jì)人和明星,生活中還有很多行為本質(zhì)就是代理模式,比如:某些大牌的飲料三級(jí)代理銷售、酒水的省市縣的代理人、三國(guó)時(shí)曹操挾天子以令諸侯等等。

說了這么多案例,都是關(guān)于代理模式的,那既然這么多人都在用代理模式,那代理模式一定解決了生活中的某些棘手的問題,那究竟是什么問題呢?

在明星和經(jīng)紀(jì)人這個(gè)案例中,因?yàn)榘汛赃@個(gè)商業(yè)行為做了細(xì)分,讓明星團(tuán)隊(duì)中每個(gè)人負(fù)責(zé)代言的一部分,使每人只需要專注于自己的事,提高每個(gè)人的專業(yè)度的同時(shí),也提高了效率,這就叫專業(yè),專人專事。

因?yàn)榻?jīng)紀(jì)人專注廣告代言的代理行為,商業(yè)經(jīng)驗(yàn)豐富,所以經(jīng)紀(jì)人也可以用他的專業(yè)知識(shí)為其他明星做廣告代言的代理,這就叫能力復(fù)用。

那么,如何使用代碼展示經(jīng)紀(jì)人代理明星的廣告行為呢?這其中有是如何運(yùn)用代理模式的呢?

類比上面的明星和經(jīng)紀(jì)人的例子:

假如有個(gè)明星類,我們想在調(diào)用明星類的代言方法之前做一些其他操作比如權(quán)限控制、記錄等,那么就需要一個(gè)中間層,先執(zhí)行中間層,在執(zhí)行明星類的代言方法。

那講到這里,想必又有人問,直接在明星類上加一個(gè)權(quán)限控制、記錄等方法不就行了么,為什么非要用代理呢?

這就是本文最重要的一個(gè)核心知識(shí),程序設(shè)計(jì)中的一個(gè)原則:類的單一性原則。這個(gè)原則很簡(jiǎn)單,就是每個(gè)類的功能盡可能單一,在這個(gè)案例中讓明星類保持功能單一,就是對(duì)代理模式的通俗解釋。

那為什么要保持類的功能單一呢?

因?yàn)橹挥泄δ軉我?,這個(gè)類被改動(dòng)的可能性才會(huì)最小,其他的操作交給其他類去辦。在這個(gè)例子中,如果在明星類里加上權(quán)限控制功能,那么明星類就不再是單一的明星類了,是明星加經(jīng)紀(jì)人兩者功能的合并類。

如果我們只想用權(quán)限控制功能,使用經(jīng)紀(jì)人的功能給其他明星篩選廣告商家,如果兩者合并,就要?jiǎng)?chuàng)建這個(gè)合并類,但是我們只使用權(quán)限功能,這就導(dǎo)致功能不單一,長(zhǎng)期功能的累加會(huì)使得代碼極為混亂,難以復(fù)用。

所以類的單一性原則和功能復(fù)用在代碼設(shè)計(jì)上很重要,這也是使用代理模式的核心。

而這整個(gè)過程所涉及到的角色可以分為四類:

  1. 主題接口:類比代言這類行為的統(tǒng)稱,是定義代理類和真實(shí)主題的公共對(duì)外方法,也是代理類代理真實(shí)主題的方法;
  2. 真實(shí)主題:類比明星這個(gè)角色,是真正實(shí)現(xiàn)業(yè)務(wù)邏輯的類;
  3. 代理類:類比經(jīng)紀(jì)人這個(gè)角色,是用來代理和封裝真實(shí)主題;
  4. Main:類比商家這個(gè)角色,是客戶端,使用代理類和主題接口完成一些工作;

在java語言的發(fā)展中,出現(xiàn)了很多種代理方式,這些代理方式可以分類為兩類:靜態(tài)代理和動(dòng)態(tài)代理,下面我們就結(jié)合代碼實(shí)例解釋下,各類代理的幾種實(shí)現(xiàn)方式,其中的優(yōu)缺點(diǎn)和適用的場(chǎng)景。

靜態(tài)代理

主題接口

package com.shuai.proxy;

public interface IDBQuery {
    String request();
}

真實(shí)主題

package com.shuai.proxy.staticproxy;

import com.shuai.proxy.IDBQuery;

public class DBQuery implements IDBQuery {

    public DBQuery() {
        try {
            Thread.sleep(1000);//假設(shè)數(shù)據(jù)庫(kù)連接等耗時(shí)操作
        } 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("在此之前,記錄下什么東西吧.....");
        //在真正需要的時(shí)候才能創(chuàng)建真實(shí)對(duì)象,創(chuàng)建過程可能很慢
        if (real == null) {
            real = new DBQuery();
        }//在多線程環(huán)境下,這里返回一個(gè)虛假類,類似于 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(); //在真正使用時(shí)才創(chuàng)建真實(shí)對(duì)象
    }
}

可以看到,主題接口是IDBQuery,真實(shí)主題是DBQuery 實(shí)現(xiàn)了IDBQuery接口,代理類是DBQueryProxy,在代理類的方法里實(shí)現(xiàn)了DBQuery類,并且在代碼里寫死了代理前后的操作,這就是靜態(tài)代理的簡(jiǎn)單實(shí)現(xiàn),可以看到靜態(tài)代理的實(shí)現(xiàn)優(yōu)缺點(diǎn)十分明顯。

靜態(tài)代理的優(yōu)缺點(diǎn):

優(yōu)點(diǎn)

使得真實(shí)主題處理的業(yè)務(wù)更加純粹,不再去關(guān)注一些公共的事情,公共的業(yè)務(wù)由代理來完成,實(shí)現(xiàn)業(yè)務(wù)的分工,公共業(yè)務(wù)發(fā)生擴(kuò)展時(shí)變得更加集中和方便。

缺點(diǎn)

這種實(shí)現(xiàn)方式很直觀也很簡(jiǎn)單,但其缺點(diǎn)是代理類必須提前寫好,如果主題接口發(fā)生了變化,代理類的代碼也要隨著變化,有著高昂的維護(hù)成本。

針對(duì)靜態(tài)代理的缺點(diǎn),是否有一種方式彌補(bǔ)?能夠不需要為每一個(gè)接口寫上一個(gè)代理方法,那就動(dòng)態(tài)代理。

動(dòng)態(tài)代理

動(dòng)態(tài)代理,在java代碼里動(dòng)態(tài)代理類使用字節(jié)碼動(dòng)態(tài)生成加載技術(shù),在運(yùn)行時(shí)生成加載類。

生成動(dòng)態(tài)代理類的方法很多,比如:JDK 自帶的動(dòng)態(tài)處理、CGLIB、Javassist、ASM 庫(kù)。

  • JDK 的動(dòng)態(tài)代理使用簡(jiǎn)單,它內(nèi)置在 JDK 中,因此不需要引入第三方 Jar 包,但相對(duì)功能比較弱。
  • CGLIB 和 Javassist 都是高級(jí)的字節(jié)碼生成庫(kù),總體性能比 JDK 自帶的動(dòng)態(tài)代理好,而且功能十分強(qiáng)大。
  • ASM 是低級(jí)的字節(jié)碼生成工具,使用 ASM 已經(jīng)近乎于在使用 Java bytecode 編程,對(duì)開發(fā)人員要求最高,當(dāng)然,也是性能最好的一種動(dòng)態(tài)代理生成工具。但 ASM 的使用很繁瑣,而且性能也沒有數(shù)量級(jí)的提升,與 CGLIB 等高級(jí)字節(jié)碼生成工具相比,ASM 程序的維護(hù)性較差,如果不是在對(duì)性能有苛刻要求的場(chǎng)合,還是推薦 CGLIB 或者 Javassist。

這里介紹兩種非常常用的動(dòng)態(tài)代理技術(shù),面試時(shí)也會(huì)常常用到的技術(shù):JDK 自帶的動(dòng)態(tài)處理CGLIB 兩種。

jDK動(dòng)態(tài)代理

Java提供了一個(gè)Proxy類,使用Proxy類的newInstance方法可以生成某個(gè)對(duì)象的代理對(duì)象,該方法需要三個(gè)參數(shù):

  1. 類裝載器【一般我們使用的是被代理類的裝載器】

  2. 指定接口【指定要被代理類的接口】

  3. 代理對(duì)象的方法里干什么事【實(shí)現(xiàn)handler接口】

初次看見會(huì)有些不理解,沒關(guān)系,下面用一個(gè)實(shí)例來詳細(xì)展示JDK動(dòng)態(tài)代理的實(shí)現(xiàn):

代理類的實(shí)現(xiàn)

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 {
        //如果第一次調(diào)用,生成真實(shí)主題
        if (realQuery == null) {
            realQuery = new DBQuery();
        }
        if ("request".equalsIgnoreCase(method.getName())) {
            System.out.println("調(diào)用前做點(diǎn)啥,助助興.....");
            Object result = method.invoke(realQuery, args);
            System.out.println("調(diào)用后做點(diǎn)啥,助助興.....");
            return result;
        } else {
            // 如果不是調(diào)用request方法,返回真實(shí)主題完成實(shí)際的操作
            return method.invoke(realQuery, args);
        }
    }

    static IDBQuery createProxy() {
        IDBQuery proxy = (IDBQuery) Proxy.newProxyInstance(
                ClassLoader.getSystemClassLoader(), //當(dāng)前類的類加載器
                new Class[]{IDBQuery.class}, //被代理的主題接口
                new DBQueryHandler() // 代理對(duì)象,這里是當(dāng)前的對(duì)象
        );
        return proxy;
    }
}

Main客戶端

package com.shuai.proxy.jdkproxy;

import com.shuai.proxy.IDBQuery;

public class Test {
        // 客戶端測(cè)試方法
    public static void main(String[] args) {
        IDBQuery idbQuery = DBQueryHandler.createProxy();
        idbQuery.request();
    }
}

用debug的方式啟動(dòng),可以看到方法被代理到代理類中實(shí)現(xiàn),在代理類中執(zhí)行真實(shí)主題的方法前后可以進(jìn)行很多操作。

雖然這種方法實(shí)現(xiàn)看起來很方便,但是細(xì)心的同學(xué)應(yīng)該也已經(jīng)觀察到了,JDK動(dòng)態(tài)代理技術(shù)的實(shí)現(xiàn)是必須要一個(gè)接口才行的,所以JDK動(dòng)態(tài)代理的優(yōu)缺點(diǎn)也非常明顯:

優(yōu)點(diǎn):

  • 不需要為真實(shí)主題寫一個(gè)形式上完全一樣的封裝類,減少維護(hù)成本;
  • 可以在運(yùn)行時(shí)制定代理類的執(zhí)行邏輯,提升系統(tǒng)的靈活性;

缺點(diǎn):

  • JDK動(dòng)態(tài)代理,真實(shí)主題 必須實(shí)現(xiàn)的主題接口,如果真實(shí)主題 沒有實(shí)現(xiàn)主圖接口,或者沒有主題接口,則不能生成代理對(duì)象。

由于必須要有接口才能使用JDK的動(dòng)態(tài)代理,那是否有一種方式可以沒有接口只有真實(shí)主題實(shí)現(xiàn)類也可以使用動(dòng)態(tài)代理呢?這就是第二種動(dòng)態(tài)代理:CGLIB

CGLIB動(dòng)態(tài)代理

使用 CGLIB 生成動(dòng)態(tài)代理,首先需要生成 Enhancer 類實(shí)例,并指定用于處理代理業(yè)務(wù)的回調(diào)類。在 Enhancer.create() 方法中,會(huì)使用 DefaultGeneratorStrategy.Generate() 方法生成動(dòng)態(tài)代理類的字節(jié)碼,并保存在 byte 數(shù)組中。接著使用 ReflectUtils.defineClass() 方法,通過反射,調(diào)用 ClassLoader.defineClass() 方法,將字節(jié)碼裝載到 ClassLoader 中,完成類的加載。最后使用 ReflectUtils.newInstance() 方法,通過反射,生成動(dòng)態(tài)類的實(shí)例,并返回該實(shí)例?;玖鞒淌歉鶕?jù)指定的回調(diào)類生成 Class 字節(jié)碼—通過 defineClass() 將字節(jié)碼定義為類—使用反射機(jī)制生成該類的實(shí)例。

真實(shí)主題

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 {

    /**
     * 創(chuàng)建代理對(duì)象
     *
     * @return
     */
    Object getBookProxyImplInstance() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(BookImpl.class);
        // 回調(diào)方法
        enhancer.setCallback(this);
        // 創(chuàng)建代理對(duì)象
        return enhancer.create();
    }

    // 回調(diào)方法
    @Override
    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;
    }
}

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的優(yōu)缺點(diǎn)

優(yōu)點(diǎn)

CGLIB通過繼承的方式進(jìn)行代理、無論目標(biāo)對(duì)象沒有沒實(shí)現(xiàn)接口都可以代理,彌補(bǔ)了JDK動(dòng)態(tài)代理的缺陷。

缺點(diǎn)

  1. CGLib創(chuàng)建的動(dòng)態(tài)代理對(duì)象性能比JDK創(chuàng)建的動(dòng)態(tài)代理對(duì)象的性能高不少,但是CGLib在創(chuàng)建代理對(duì)象時(shí)所花費(fèi)的時(shí)間卻比JDK多得多,所以對(duì)于單例的對(duì)象,因?yàn)闊o需頻繁創(chuàng)建對(duì)象,用CGLib合適,反之,使用JDK方式要更為合適一些。
  2. 由于CGLib由于是采用動(dòng)態(tài)創(chuàng)建子類的方法,對(duì)于final方法,無法進(jìn)行代理。

代理模式的應(yīng)用場(chǎng)合

代理模式有多種應(yīng)用場(chǎng)合,如下所述:

  1. 遠(yuǎn)程代理,也就是為一個(gè)對(duì)象在不同的地址空間提供局部代表,這樣可以隱藏一個(gè)對(duì)象存在于不同地址空間的事實(shí)。比如說 WebService,當(dāng)我們?cè)趹?yīng)用程序的項(xiàng)目中加入一個(gè) Web 引用,引用一個(gè) WebService,此時(shí)會(huì)在項(xiàng)目中聲稱一個(gè) WebReference 的文件夾和一些文件,這個(gè)就是起代理作用的,這樣可以讓那個(gè)客戶端程序調(diào)用代理解決遠(yuǎn)程訪問的問題;
  2. 虛擬代理,是根據(jù)需要?jiǎng)?chuàng)建開銷很大的對(duì)象,通過它來存放實(shí)例化需要很長(zhǎng)時(shí)間的真實(shí)對(duì)象。這樣就可以達(dá)到性能的最優(yōu)化,比如打開一個(gè)網(wǎng)頁,這個(gè)網(wǎng)頁里面包含了大量的文字和圖片,但我們可以很快看到文字,但是圖片卻是一張一張地下載后才能看到,那些未打開的圖片框,就是通過虛擬代里來替換了真實(shí)的圖片,此時(shí)代理存儲(chǔ)了真實(shí)圖片的路徑和尺寸;
  3. 安全代理,用來控制真實(shí)對(duì)象訪問時(shí)的權(quán)限。一般用于對(duì)象應(yīng)該有不同的訪問權(quán)限的時(shí)候;
  4. 指針引用,是指當(dāng)調(diào)用真實(shí)的對(duì)象時(shí),代理處理另外一些事。比如計(jì)算真實(shí)對(duì)象的引用次數(shù),這樣當(dāng)該對(duì)象沒有引用時(shí),可以自動(dòng)釋放它,或當(dāng)?shù)谝淮我靡粋€(gè)持久對(duì)象時(shí),將它裝入內(nèi)存,或是在訪問一個(gè)實(shí)際對(duì)象前,檢查是否已經(jīng)釋放它,以確保其他對(duì)象不能改變它。這些都是通過代理在訪問一個(gè)對(duì)象時(shí)附加一些內(nèi)務(wù)處理;
  5. 延遲加載,用代理模式實(shí)現(xiàn)延遲加載的一個(gè)經(jīng)典應(yīng)用就在 Hibernate 框架里面。當(dāng) Hibernate 加載實(shí)體 bean 時(shí),并不會(huì)一次性將數(shù)據(jù)庫(kù)所有的數(shù)據(jù)都裝載。默認(rèn)情況下,它會(huì)采取延遲加載的機(jī)制,以提高系統(tǒng)的性能。Hibernate 中的延遲加載主要分為屬性的延遲加載和關(guān)聯(lián)表的延時(shí)加載兩類。實(shí)現(xiàn)原理是使用代理攔截原有的 getter 方法,在真正使用對(duì)象數(shù)據(jù)時(shí)才去數(shù)據(jù)庫(kù)或者其他第三方組件加載實(shí)際的數(shù)據(jù),從而提升系統(tǒng)性能。

參考:
代理模式原理及實(shí)例講解
為什么使用代理模式

歡迎關(guān)注公眾號(hào):java之旅

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容