細(xì)聊代理模式

如果要說設(shè)計(jì)模式中哪種模式在生活中最常見工碾,那么代理模式是當(dāng)仁不讓的。比如踪央,你一覺睡到中午不想去食堂打飯臀玄,于是你委托阿黃讓他幫你把飯帶回來,這就產(chǎn)生了一種代理關(guān)系畅蹂,其中委托人是你健无,代理人是阿黃,而事件就是打飯液斜,你為了足不出戶就能吃到午飯而想到的這餿主意累贤,就是一種代理方法。又比如旗唁,你女朋友從外地回來下飛機(jī)了畦浓,而你又因?yàn)橐恍┎豢擅枋龅氖虑椴荒苋ソ訖C(jī),這時候你就可以叫你的好基友去機(jī)場幫你把女朋友接回來啊检疫,于是你和你好基友就形成了一種代理關(guān)系讶请,委托人是你,代理人是好基友屎媳,而事件就是接女朋友回來夺溢,這種很有可能戴綠帽子的方法就是一種代理方法。然后烛谊,將這些代理方法總結(jié)規(guī)律风响,抽象出來,上升到一個通用層面的高度丹禀,就是代理模式状勤。什么是代理模式呢?通俗地講双泪,有些事情本應(yīng)該是你做的持搜,但是你不想做或者不方便做的時候,叫別人替你完成焙矛。生活中種種“幫幫忙”的事件葫盼,都有代理模式的影子,所以說代理模式在生活中很常見村斟。

靜態(tài)代理

那么我們?nèi)绾问褂么a來描述代理模式呢贫导?我們將上面的例子總結(jié)規(guī)律抛猫,抽象出來,就大概有下面兩點(diǎn):

  • 委托人和代理人都要完成同樣的事情(共同實(shí)現(xiàn)事件的接口)孩灯。
  • 代理人要做的這件事其實(shí)是委托人請求的闺金,也就是說,代理人本身并不知道如何做這件事峰档,只有委托人告訴他了掖看,代理人才知道怎么做。所以面哥,代理人需要持有委托人的引用,在做這件事的時候就按照委托人的意愿去做毅待。

就比如第一個例子尚卫,“打飯”這件事情,由于你和阿黃都能做尸红,所以可以寫成一個接口吱涉,讓你和阿黃都能實(shí)現(xiàn)。

/**
 * 一個抽象的午飯類, 具體要吃什么樣午飯, 看子類的心情外里。
 */
public interface Lunch {

    /**
     * 吃什么樣的午飯, 糖醋排骨?宮保雞丁?怎爵。。盅蝗。鳖链。
     */
    void catagory();
}

然后就要創(chuàng)建“你”這個對象了,由于你要吃午飯墩莫,就需要實(shí)現(xiàn)Lunch類:

/**
 * 你, 需要考慮午飯吃什么
 */
public class You implements Lunch {
    @Override
    public void catagory() {
        // 你想吃宮保雞丁
        System.out.println("我要吃宮保雞丁蓋飯");
    }
}

然后你很懶啊芙委,需要找阿黃這個代理人幫你帶午飯回來,所以還需要一個代理人對象狂秦,注意灌侣,由于代理事件是委托人(“你”)下發(fā)的,所以代理人對象里面還需要持有委托人的應(yīng)用:

/**
 * 代理人, 由于需要幫委托人帶飯, 也必須實(shí)現(xiàn)午飯接口
 */
public class ProxyMan implements Lunch {
    /**
     * 必須是要有委托人的存在才行,沒有委托人還需要代理人干嘛?
     */
    private You mYou;
    /**
     * 在創(chuàng)建代理人對象的時候就傳入一個委托人進(jìn)來裂问,這樣就使得代理人可以持有委托人的引用了侧啼。
     * 當(dāng)然這只是一種方法,你也可以使用其他方法讓代理人持有委托人的引用堪簿。
     */
    public ProxyMan(You you) {
        mYou = you;
    }
    @Override
    public void catagory() {
        // "你"要讓代理人幫你做的事情痊乾。
        mYou.catagory();
    }
}

OK,大功告成戴甩,然后我們創(chuàng)建一個場景(客戶端)來模擬一下這個事情:

public class Proxy {

    public static void main(String[] args) {
        // 創(chuàng)建一個委托人
        You you = new You();
        // 創(chuàng)建一個代理人, 可以是阿黃, 也可以是其他人
        ProxyMan aHuang = new ProxyMan(you);
        // 讓阿黃幫你帶飯符喝。
        aHuang.catagory();
    }
}

以上便是代理模式的一種模版,結(jié)合代碼甜孤,我們可以總結(jié)出代理模式的類圖如下:

Class Diagram (1).png-12.5kB
Class Diagram (1).png-12.5kB

好了协饲,扯了這么多畏腕,大概對代理模式有一個印象了。是時候給代理模式來下一個正式的定義了:為其他對象提供一種代理以控制對這個對象的訪問茉稠。

另外描馅,我們現(xiàn)在看到的代理模式,又稱之為靜態(tài)代理而线,因?yàn)榇眍愂切枰覀冏约菏謩泳帉懙拿郏竺嫖覀儠務(wù)勅绾尾挥檬謩泳帉懘眍悾瑏韺?shí)現(xiàn)代理模式膀篮。

靜態(tài)代理的作用

當(dāng)然嘹狞,你會說,這種小事誓竿,委托類自己就能完成的嘛磅网,非要多增加一些類干嘛。的確筷屡,如果單是這種簡單的例子涧偷,使用任何設(shè)計(jì)模式都是多此一舉。但在實(shí)際工作中毙死,使用代理模式就會有如下的有點(diǎn):

  • 解耦燎潮。這幾乎是設(shè)計(jì)模式解決的根本問題。在這里扼倘,委托類可以專心地做好自己确封,麻煩的事情讓別人代勞。
  • 攔截再菊、擴(kuò)展方法隅肥。代理類在實(shí)現(xiàn)接口方法的時候,除了直接調(diào)用委托類的方法外袄简,還可以在不修改委托類的情況下腥放,增加一些其他的功能,比如順便帶瓶可樂回來绿语?

為了能讓我們的代理人阿黃能帶瓶可樂回來秃症,我們只需要在代理類中對catagory()方法增加一個“帶瓶可樂回來”的功能即可:

public class ProxyMan implements Lunch {
   
    private You mYou;
  
    public ProxyMan(You you) {
        mYou = you;
    }
    @Override
    public void catagory() {
        // "你"要讓代理人幫你做的事情。
        mYou.catagory();
        
        // 打完飯后再去買瓶可樂
        System.out.println("嗯吕粹,買瓶可樂來喝");
    }
}

現(xiàn)在你應(yīng)該能明顯地感受到代理類的作用了种柑。對,就是攔截方法匹耕、對委托的方法進(jìn)行加工處理聚请。

動態(tài)代理

然后有一天,當(dāng)你中午醒來的時候,發(fā)現(xiàn)寢室里除了你以外空無一人驶赏,你為了雙倍經(jīng)驗(yàn)雙倍金幣所以不想浪費(fèi)時間下樓去食堂打飯炸卑,但是你餓啊,要吃飯啊煤傍,這時你就只有等寢室再進(jìn)來個人盖文,讓他幫你帶飯(假設(shè)你不會叫外賣)。但是這會有一個問題蚯姆,之前一醒來就可以讓阿黃幫你帶飯五续,是因?yàn)榘ⅫS在你醒來之前就在寢室的(程序編譯階段就存在一個代理類),并且設(shè)定了阿黃“能打飯”(實(shí)現(xiàn)了Lunch接口)這個屬性龄恋,所以簡單的幾行代碼就能搞定疙驾。但是現(xiàn)在,你醒來之后(程序開始運(yùn)行)郭毕,發(fā)現(xiàn)并沒有人可以幫你帶飯(沒有一個在編譯階段就實(shí)現(xiàn)了Lunch接口的代理類)荆萤,在你等待的過程中也并不知道會是誰進(jìn)來,阿黃铣卡,阿貓還是阿三(我們需要在程序運(yùn)行的時候動態(tài)生成一個對象)?而且最重要的是偏竟,進(jìn)來的那個家伙能幫你帶飯才行(運(yùn)行階段生成的對象要實(shí)現(xiàn)Lunch接口)煮落。

那么我們再梳理一遍,現(xiàn)在的你應(yīng)該怎么做才能吃到午飯呢踊谋?

  • 首先蝉仇,“你”這個對象和“午飯”這個事件是必須存在的。
  • 其次殖蚕,創(chuàng)建一個代理人轿衔。由于這個代理人是幫你做事情的,所以創(chuàng)建這個代理人的方法必須和“你”這個對象以及“午飯”事件接口相關(guān)睦疫。
  • 最后害驹,讓代理人去幫你打飯回來。

讓我們來用代碼實(shí)現(xiàn)一下蛤育,You這個委托類和Lunch接口是要保留的宛官,而ProxyMan這個代理類就不需要了。在場景中(client客戶端)瓦糕,我們可以使用代理的一個靜態(tài)方法newProxyInstance來創(chuàng)建一個代理對象底洗,這個代理對象當(dāng)然是實(shí)現(xiàn)了Lunch這個接口的。那么client中的代碼如下:

public static void main(String[] args) {
    // 創(chuàng)建一個委托人
    You you = new You();
    // 創(chuàng)建一個代理人, 第一個參數(shù)是委托類的ClassLoader, 第二個參數(shù)是要實(shí)現(xiàn)的接口數(shù)組,
    // 第三個參數(shù)是一個匿名內(nèi)部類, 對于代理人將要做的任何事情做攔截處理咕娄。
    Lunch somebodyCouldTakeLunch = (Lunch) Proxy.newProxyInstance(you.getClass().getClassLoader(), new Class[]{Lunch.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            return method.invoke(you, args);
        }
    });
    //  動態(tài)生成的代理人去帶飯了
    somebodyCouldTakeLunch.catagory();
}

當(dāng)動態(tài)生成的代理人對象調(diào)用方法去帶飯時(somebodyCouldTakeLunch.catagory())亥揖,就會觸發(fā)匿名內(nèi)部類InvocationHandler中的invoke()方法,這里面沒有做其他處理圣勒,直接返回了“你”這個對象的對應(yīng)方法费变。也就是說摧扇,動態(tài)生成的代理人是安裝你的要求帶回來宮爆雞丁蓋飯!

一個簡單的動態(tài)代理就這么完成了胡控。然而動態(tài)代理是想當(dāng)強(qiáng)大的扳剿,為了顯示這種強(qiáng)大,這里再舉一個例子昼激,List集合大家都用過庇绽,這個接口有很多方法,如果我們想要屏蔽remove這個方法橙困,這時我們就可以使用動態(tài)代理瞧掺,來生成一個代理類List,每當(dāng)調(diào)用到代理類的remove()方法時凡傅,就跑出異常辟狈,實(shí)現(xiàn)代碼如下:

public static List getList(List list) {
        return (List) Proxy.newProxyInstance(List.class.getClassLoader(), new Class[]{List.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (method.getName().equals("remove")) {
                    throw new UnsupportedOperationException();
                } else {
                    return method.invoke(list, args);
                }
            }
        });
    }

當(dāng)我們傳入一個List對象進(jìn)去,返回的就是經(jīng)過處理后的代理List類夏跷,這時如果調(diào)用代理List的remove方法時就會報(bào)錯了哼转。

估計(jì)你已經(jīng)知道了如何使用動態(tài)代理了,但是你肯定有很多疑惑槽华,Proxy.newProxyInstance這個靜態(tài)方法是怎么完成這件事的呢壹蔓?我們看一下源碼(以下分析的源碼是基于jdk 1.6,相比起來比較簡單易懂):

public static Object newProxyInstance(ClassLoader loader, 
            Class<?>[] interfaces, 
            InvocationHandler h) 
            throws IllegalArgumentException { 
    
    // 檢查 h 不為空猫态,否則拋異常
    if (h == null) { 
        throw new NullPointerException(); 
    } 

    // 獲得與制定類裝載器和一組接口相關(guān)的代理類類型對象
    Class cl = getProxyClass(loader, interfaces); 

    // 通過反射獲取構(gòu)造函數(shù)對象并生成代理類實(shí)例
    try { 
        Constructor cons = cl.getConstructor(constructorParams); 
        return (Object) cons.newInstance(new Object[] { h }); 
    } catch (NoSuchMethodException e) { throw new InternalError(e.toString()); 
    } catch (IllegalAccessException e) { throw new InternalError(e.toString()); 
    } catch (InstantiationException e) { throw new InternalError(e.toString()); 
    } catch (InvocationTargetException e) { throw new InternalError(e.toString()); 
    } 
}

其他都好懂佣蓉,可能就是getProxyClass()這個方法沒見過。同時該方法也是動態(tài)代理中最核心的東西亲雪。由于該方法比較長勇凭,去除注釋后也有120+行代碼,所以我就不粘貼代碼了义辕,你可以自己去Java源碼中去尋找虾标,或者你可以查看這個鏈接: getProxyClass(),里面大概的流程是:先將傳入的接口數(shù)組進(jìn)行一系列檢查灌砖,并存儲所有接口名稱夺巩,然后創(chuàng)建一個HashMap的緩存表,里面以不同的鍵值對形式來存放著待創(chuàng)建的代理類周崭、正在被創(chuàng)建的代理類柳譬。然后根據(jù)一系列的規(guī)則確定包名,調(diào)用ProxyGenerator.generateProxyClass()方法創(chuàng)建代理類续镇,這個方法會調(diào)用ProxyGenerator對象的generateClassFile()方法來生成代理類類文件的字節(jié)數(shù)組美澳,具體生成方法可以參見這個鏈接中的源碼: generateClassFile(),我就不再深入闡述了。生成代理類對象后制跟,將其記錄到proxyClasses表中舅桩,然后就是更新緩存表、清楚緩存表等一系列善后工作了雨膨。

由于所有的代理類都有一個共同的父類Proxy擂涛,Java 的繼承機(jī)制注定了這些動態(tài)代理類們無法實(shí)現(xiàn)對class的動態(tài)代理,原因是多繼承在 Java 中本質(zhì)上就行不通聊记。所以Java只能進(jìn)行接口的動態(tài)代理撒妈,這不得不說是一個遺憾。

靜態(tài)代理和動態(tài)代理的聯(lián)系

然而在大多數(shù)情況下排监,我們的代理類在敲代碼的時候就能確定下來狰右,也就是說,我們?nèi)粘i_發(fā)的大多數(shù)情況都能使用靜態(tài)代理舆床。那么能不能用動態(tài)代理呢棋蚌?當(dāng)然能!為什么要用動態(tài)代理呢挨队?

當(dāng)委托類實(shí)現(xiàn)的接口方法比較多的時候谷暮,寫一個代理類一個個地處理委托方法就太麻煩了。這時我們可以在invoke()方法中對委托類的所有方法進(jìn)行判斷盛垦、攔截湿弦、擴(kuò)展等處理。減少代碼量情臭。

Retrofit中的動態(tài)代理

Retrofit可謂是當(dāng)下最火的Android網(wǎng)絡(luò)請求框架之一了,我們在Retrofit的wiki上可以看到Retrofit的使用方法如下:

1.創(chuàng)建一個請求方法的接口:

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

2.生成Retrofit對象赌蔑,并且創(chuàng)建一個實(shí)現(xiàn)了GitHubServiece接口的實(shí)體類:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);

3.發(fā)起網(wǎng)絡(luò)請求:

Call<List<Repo>> repos = service.listRepos("octocat");

我們看第二步俯在,傳入了一個接口對象,然后就創(chuàng)建了一個實(shí)現(xiàn)了GitHubService接口的實(shí)體類娃惯?怎么實(shí)現(xiàn)的跷乐?Excuse me?這時候趾浅,就需要輕輕地點(diǎn)擊去看源碼:

public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();

          @Override public Object invoke(Object proxy, Method method, Object... args)
              throws Throwable {
            // If the method is a method from Object then defer to normal invocation.
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            if (platform.isDefaultMethod(method)) {

              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            ServiceMethod serviceMethod = loadServiceMethod(method);
            OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
}

一個大寫的動態(tài)代理c堤帷!皿哨!當(dāng)然這里用的并不是規(guī)范的代理模式浅侨,因?yàn)槲覀冎溃砟J绞切枰幸粋€具體的委托類的证膨,但是這里并沒有一個具體的委托類如输,它是直接將傳入的GitHubService接口,既作為事件接口,又作為代理類使用不见,顯然這里攔截了接口中的方法澳化,并不能對該接口的方法本身進(jìn)行任何操作,這里使用動態(tài)代理稳吮,純粹是為了攔截方法缎谷,獲取接口方法上的注解信息,然后返回一個Adapter灶似。這種可以算得上一個動態(tài)代理的擴(kuò)展應(yīng)用吧列林。

靜態(tài)代理和動態(tài)代理的應(yīng)用場景很多,常見的框架都有用到(比如Spring框架喻奥、OrmLite框架)席纽,希望讀完本篇文章能幫你理解靜態(tài)代理和動態(tài)代理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末撞蚕,一起剝皮案震驚了整個濱河市润梯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌甥厦,老刑警劉巖纺铭,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異刀疙,居然都是意外死亡舶赔,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門谦秧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來竟纳,“玉大人,你說我怎么就攤上這事疚鲤∽独郏” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵集歇,是天一觀的道長桶略。 經(jīng)常有香客問我,道長诲宇,這世上最難降的妖魔是什么际歼? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮姑蓝,結(jié)果婚禮上鹅心,老公的妹妹穿的比我還像新娘。我一直安慰自己纺荧,他們只是感情好巴帮,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布溯泣。 她就那樣靜靜地躺著,像睡著了一般榕茧。 火紅的嫁衣襯著肌膚如雪垃沦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天用押,我揣著相機(jī)與錄音彩匕,去河邊找鬼垦页。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哄辣。 我是一名探鬼主播滚秩,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼澎语,長吁一口氣:“原來是場噩夢啊……” “哼蜗顽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起血崭,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤卧惜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后夹纫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咽瓷,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年舰讹,在試婚紗的時候發(fā)現(xiàn)自己被綠了茅姜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡月匣,死狀恐怖钻洒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锄开,我是刑警寧澤素标,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站院刁,受9級特大地震影響糯钙,放射性物質(zhì)發(fā)生泄漏粪狼。R本人自食惡果不足惜退腥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望再榄。 院中可真熱鬧狡刘,春花似錦、人聲如沸困鸥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至澜术,卻和暖如春艺蝴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鸟废。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工猜敢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盒延。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓缩擂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親添寺。 傳聞我的和親對象是個殘疾皇子胯盯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評論 2 348

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

  • 設(shè)計(jì)模式匯總 一、基礎(chǔ)知識 1. 設(shè)計(jì)模式概述 定義:設(shè)計(jì)模式(Design Pattern)是一套被反復(fù)使用计露、多...
    MinoyJet閱讀 3,922評論 1 15
  • 整體Retrofit內(nèi)容如下: 1博脑、Retrofit解析1之前哨站——理解RESTful 2、Retrofit解析...
    隔壁老李頭閱讀 3,232評論 2 10
  • 國家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說閱讀 10,925評論 6 13
  • 開始學(xué)習(xí)做產(chǎn)品的第一天薄坏。 忍不住吐個槽趋厉,簡書能不能把這個新建文章欄做成鼠標(biāo)不在此欄時自動隱藏... 從...
    duXinG丶閱讀 192評論 0 1
  • 我的發(fā)小、老同學(xué)耿玲霞的感人事跡胶坠,能得到郭柏生君账、仇建軍、宋松柏沈善、宋亞琴乡数、耿同心、耿有民闻牡、高娟净赴、郭亞玲、范秉輝...
    耿平海閱讀 821評論 9 8