細聊代理模式

如果要說設計模式中哪種模式在生活中最常見,那么代理模式是當仁不讓的。比如,你一覺睡到中午不想去食堂打飯,于是你委托阿黃讓他幫你把飯帶回來,這就產生了一種代理關系,其中委托人是你,代理人是阿黃,而事件就是打飯,你為了足不出戶就能吃到午飯而想到的這餿主意,就是一種代理方法。又比如,你女朋友從外地回來下飛機了,而你又因為一些不可描述的事情不能去接機,這時候你就可以叫你的好基友去機場幫你把女朋友接回來啊,于是你和你好基友就形成了一種代理關系,委托人是你,代理人是好基友,而事件就是接女朋友回來,這種很有可能戴綠帽子的方法就是一種代理方法。然后,將這些代理方法總結規律,抽象出來,上升到一個通用層面的高度,就是代理模式。什么是代理模式呢?通俗地講,有些事情本應該是你做的,但是你不想做或者不方便做的時候,叫別人替你完成。生活中種種“幫幫忙”的事件,都有代理模式的影子,所以說代理模式在生活中很常見。

靜態代理

那么我們如何使用代碼來描述代理模式呢?我們將上面的例子總結規律,抽象出來,就大概有下面兩點:

  • 委托人和代理人都要完成同樣的事情(共同實現事件的接口)。
  • 代理人要做的這件事其實是委托人請求的,也就是說,代理人本身并不知道如何做這件事,只有委托人告訴他了,代理人才知道怎么做。所以,代理人需要持有委托人的引用,在做這件事的時候就按照委托人的意愿去做。

就比如第一個例子,“打飯”這件事情,由于你和阿黃都能做,所以可以寫成一個接口,讓你和阿黃都能實現。

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

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

然后就要創建“你”這個對象了,由于你要吃午飯,就需要實現Lunch類:

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

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

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

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

public class Proxy {

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

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

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

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

另外,我們現在看到的代理模式,又稱之為靜態代理,因為代理類是需要我們自己手動編寫的,后面我們會談談如何不用手動編寫代理類,來實現代理模式。

靜態代理的作用

當然,你會說,這種小事,委托類自己就能完成的嘛,非要多增加一些類干嘛。的確,如果單是這種簡單的例子,使用任何設計模式都是多此一舉。但在實際工作中,使用代理模式就會有如下的有點:

  • 解耦。這幾乎是設計模式解決的根本問題。在這里,委托類可以專心地做好自己,麻煩的事情讓別人代勞。
  • 攔截、擴展方法。代理類在實現接口方法的時候,除了直接調用委托類的方法外,還可以在不修改委托類的情況下,增加一些其他的功能,比如順便帶瓶可樂回來?

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

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

現在你應該能明顯地感受到代理類的作用了。對,就是攔截方法、對委托的方法進行加工處理。

動態代理

然后有一天,當你中午醒來的時候,發現寢室里除了你以外空無一人,你為了雙倍經驗雙倍金幣所以不想浪費時間下樓去食堂打飯,但是你餓啊,要吃飯啊,這時你就只有等寢室再進來個人,讓他幫你帶飯(假設你不會叫外賣)。但是這會有一個問題,之前一醒來就可以讓阿黃幫你帶飯,是因為阿黃在你醒來之前就在寢室的(程序編譯階段就存在一個代理類),并且設定了阿黃“能打飯”(實現了Lunch接口)這個屬性,所以簡單的幾行代碼就能搞定。但是現在,你醒來之后(程序開始運行),發現并沒有人可以幫你帶飯(沒有一個在編譯階段就實現了Lunch接口的代理類),在你等待的過程中也并不知道會是誰進來,阿黃,阿貓還是阿三(我們需要在程序運行的時候動態生成一個對象)?而且最重要的是,進來的那個家伙能幫你帶飯才行(運行階段生成的對象要實現Lunch接口)。

那么我們再梳理一遍,現在的你應該怎么做才能吃到午飯呢?

  • 首先,“你”這個對象和“午飯”這個事件是必須存在的。
  • 其次,創建一個代理人。由于這個代理人是幫你做事情的,所以創建這個代理人的方法必須和“你”這個對象以及“午飯”事件接口相關。
  • 最后,讓代理人去幫你打飯回來。

讓我們來用代碼實現一下,You這個委托類和Lunch接口是要保留的,而ProxyMan這個代理類就不需要了。在場景中(client客戶端),我們可以使用代理的一個靜態方法newProxyInstance來創建一個代理對象,這個代理對象當然是實現了Lunch這個接口的。那么client中的代碼如下:

public static void main(String[] args) {
    // 創建一個委托人
    You you = new You();
    // 創建一個代理人, 第一個參數是委托類的ClassLoader, 第二個參數是要實現的接口數組,
    // 第三個參數是一個匿名內部類, 對于代理人將要做的任何事情做攔截處理。
    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);
        }
    });
    //  動態生成的代理人去帶飯了
    somebodyCouldTakeLunch.catagory();
}

當動態生成的代理人對象調用方法去帶飯時(somebodyCouldTakeLunch.catagory()),就會觸發匿名內部類InvocationHandler中的invoke()方法,這里面沒有做其他處理,直接返回了“你”這個對象的對應方法。也就是說,動態生成的代理人是安裝你的要求帶回來宮爆雞丁蓋飯!

一個簡單的動態代理就這么完成了。然而動態代理是想當強大的,為了顯示這種強大,這里再舉一個例子,List集合大家都用過,這個接口有很多方法,如果我們想要屏蔽remove這個方法,這時我們就可以使用動態代理,來生成一個代理類List,每當調用到代理類的remove()方法時,就跑出異常,實現代碼如下:

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);
                }
            }
        });
    }

當我們傳入一個List對象進去,返回的就是經過處理后的代理List類,這時如果調用代理List的remove方法時就會報錯了。

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

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

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

    // 通過反射獲取構造函數對象并生成代理類實例
    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()這個方法沒見過。同時該方法也是動態代理中最核心的東西。由于該方法比較長,去除注釋后也有120+行代碼,所以我就不粘貼代碼了,你可以自己去Java源碼中去尋找,或者你可以查看這個鏈接: getProxyClass(),里面大概的流程是:先將傳入的接口數組進行一系列檢查,并存儲所有接口名稱,然后創建一個HashMap的緩存表,里面以不同的鍵值對形式來存放著待創建的代理類、正在被創建的代理類。然后根據一系列的規則確定包名,調用ProxyGenerator.generateProxyClass()方法創建代理類,這個方法會調用ProxyGenerator對象的generateClassFile()方法來生成代理類類文件的字節數組,具體生成方法可以參見這個鏈接中的源碼: generateClassFile(),我就不再深入闡述了。生成代理類對象后,將其記錄到proxyClasses表中,然后就是更新緩存表、清楚緩存表等一系列善后工作了。

由于所有的代理類都有一個共同的父類Proxy,Java 的繼承機制注定了這些動態代理類們無法實現對class的動態代理,原因是多繼承在 Java 中本質上就行不通。所以Java只能進行接口的動態代理,這不得不說是一個遺憾。

靜態代理和動態代理的聯系

然而在大多數情況下,我們的代理類在敲代碼的時候就能確定下來,也就是說,我們日常開發的大多數情況都能使用靜態代理。那么能不能用動態代理呢?當然能!為什么要用動態代理呢?

當委托類實現的接口方法比較多的時候,寫一個代理類一個個地處理委托方法就太麻煩了。這時我們可以在invoke()方法中對委托類的所有方法進行判斷、攔截、擴展等處理。減少代碼量。

Retrofit中的動態代理

Retrofit可謂是當下最火的Android網絡請求框架之一了,我們在Retrofit的wiki上可以看到Retrofit的使用方法如下:

1.創建一個請求方法的接口:

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

2.生成Retrofit對象,并且創建一個實現了GitHubServiece接口的實體類:

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

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

3.發起網絡請求:

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

我們看第二步,傳入了一個接口對象,然后就創建了一個實現了GitHubService接口的實體類?怎么實現的?Excuse me?這時候,就需要輕輕地點擊去看源碼:

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);
          }
        });
}

一個大寫的動態代理!!!當然這里用的并不是規范的代理模式,因為我們知道,代理模式是需要有一個具體的委托類的,但是這里并沒有一個具體的委托類,它是直接將傳入的GitHubService接口,既作為事件接口,又作為代理類使用,顯然這里攔截了接口中的方法,并不能對該接口的方法本身進行任何操作,這里使用動態代理,純粹是為了攔截方法,獲取接口方法上的注解信息,然后返回一個Adapter。這種可以算得上一個動態代理的擴展應用吧。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 設計模式匯總 一、基礎知識 1. 設計模式概述 定義:設計模式(Design Pattern)是一套被反復使用、多...
    MinoyJet閱讀 3,978評論 1 15
  • 整體Retrofit內容如下: 1、Retrofit解析1之前哨站——理解RESTful 2、Retrofit解析...
    隔壁老李頭閱讀 3,269評論 2 10
  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,167評論 6 13
  • 開始學習做產品的第一天。 忍不住吐個槽,簡書能不能把這個新建文章欄做成鼠標不在此欄時自動隱藏... 從...
    duXinG丶閱讀 208評論 0 1
  • 我的發小、老同學耿玲霞的感人事跡,能得到郭柏生、仇建軍、宋松柏、宋亞琴、耿同心、耿有民、高娟、郭亞玲、范秉輝...
    耿平海閱讀 850評論 9 8