代理模式(控制對象訪問)

提綱

最近在讀 Android Binder 部分的源碼贫堰,之前三三兩兩的讀過一些片段廷粒。但總是感覺理解的不深刻享扔,在讀源碼的過程中看到了代理模式的應用狱意,那便把代理模式單獨開一章描述清楚筋量,需要查看其它設計模式描述可以查看我的文章《設計模式開篇》窿克。

本篇文章將根據(jù)以下知識點展開描述:

1、普通代理模式(分析 Java 文件操作源碼)
2毛甲、遠程代理模式(分析 Android Binder Service 源碼)
3年叮、動態(tài)代理實現(xiàn)(分析 API 模塊設計)

普通代理模式

使用java.io.File來形容代理模式的本質是再恰當不過的事情了,為了保證上下文的連貫性玻募,請容許我設計一個文件操作的場景只损。

假使你需要使用批復同事轉發(fā)給你的文件,你使用程序讀取出文件內容七咧,等你閱讀完畢后你會往文件中加入你的意見跃惫。在批復完成后,你會將文件通過郵件回復給同事艾栋,并同事刪除本地的備份爆存。

在動工之前假設你會考慮如下情景:

  • 文件是否為空
  • 是否有權限讀取文件
  • 是否有權限寫入文件
  • 刪除文件

文件操作 JDK 已經(jīng)為我們內置好了自然不用我們重復開發(fā)輪子,讓我們看看這部分的代碼蝗砾。

public class File
    implements Serializable, Comparable<File>
{
    public long length() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkRead(path);
        }
        if (isInvalid()) {
            return 0L;
        }
        return fs.getLength(this);
    }

    public boolean canRead() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkRead(path);
        }
        if (isInvalid()) {
            return false;
        }
        return fs.checkAccess(this, FileSystem.ACCESS_READ);
    }

    public boolean canWrite() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkWrite(path);
        }
        if (isInvalid()) {
            return false;
        }
        return fs.checkAccess(this, FileSystem.ACCESS_WRITE);
    }

    public boolean delete() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkDelete(path);
        }
        if (isInvalid()) {
            return false;
        }
        return fs.delete(this);
    }
}

我們發(fā)現(xiàn)java.io.File這個類并沒有真正的涉及到文件的操作先较,而只是對真正的操作的一層包裝。比如每個方法中都使用了SecurityManager做安全檢測悼粮,而在檢測通過時又都使用FileSystem的實例fs調用到真正的實現(xiàn)闲勺。

FileSystem是抽象類,它定義了所有File類會調用到的底層的實現(xiàn)扣猫,比如下面的 delete()方法菜循。

abstract class FileSystem {
        public abstract boolean delete(File f);
}

我們來跟蹤下FileSystem的子類,顯示它支持了 Unix 與 Window 兩種文件系統(tǒng)申尤。讓我們跟進到UnixFileSystem里看看到底發(fā)生了什么癌幕?

FileSystem的子類們
class UnixFileSystem extends FileSystem {

   public boolean delete(File f) {
        // Keep canonicalization caches in sync after file deletion
        // and renaming operations. Could be more clever than this
        // (i.e., only remove/update affected entries) but probably
        // not worth it since these entries expire after 30 seconds
        // anyway.
        cache.clear();
        javaHomePrefixCache.clear();
        return delete0(f);
    }
    private native boolean delete0(File f);
}

看來UnixFileSystem調用了本地native方法完成了對文件的刪除操作衙耕。

分析到這里我們發(fā)現(xiàn)了上層的File文件實際上并沒有完成任何的文件的操作,而只是對FileSystem的封裝調用+權限檢查勺远。如果你仔細閱讀我貼出的代碼臭杰,你會發(fā)現(xiàn)FileSystem類本身或其子類的訪問權限都是包訪問權限,而這恰恰佐證了代理模式的本質——控制對象訪問谚中。

代理模式的本質:控制對象訪問渴杆。

具有控制對象訪問思想特征設計模式有很多種,比如:中介宪塔、門面磁奖,甚至單例都具備該特征,代理模式在某種程度而言比其它表現(xiàn)方式更純粹某筐。

遠程代理模式

在有了普通代理模式的基礎比搭,我們接下去分析說明是遠程代理模式。其實遠程代理與普通代理的差距很小南誊, 以 `File``作為例子身诺,普通代理模式的調用圖如下:

普通代理模式

而遠程代理模式與普通代理模式的區(qū)別是:有別于普通代理模式的本地調用轉發(fā),遠程代理模式使用 遠程協(xié)議 描述了 File --> FileSystem 的轉發(fā)過程抄囚。

很好的參考例子是 Android 的 Binder 部分霉赡,我們這里將貼出部分的相關代碼。不知是否是為了區(qū)分遠程代理與普通代理幔托,Android 中的遠程代理總習慣使用Stub而不是Proxy穴亏。

IWindowManager為例:

public interface IWindowManager extends android.os.IInterface{

    public static abstract class Stub extends android.os.Binder implements android.view.IWindowManager{
          // 省略部分代碼
    }

}

Stub實現(xiàn)接口IWindowManagerStub同時又繼承自BinderBinder具備遠程通訊的能力重挑。所以可以稱StubIWindowManager接口實例的遠程代理嗓化。

遠程代理模式

上圖展示了接口IWindowManagerImpl的繼承結構,很容易聯(lián)想到這是代理模式的實現(xiàn)谬哀。那我們看下這三個類之間的關系:
1刺覆、IWindowManagerImpl 是客戶端窗口管理職責的實現(xiàn)類,它提供了窗口管理等一系列操作史煎。
2谦屑、WindowManagerServiceandroid.view.IWindowManager.Stub的實現(xiàn)類,它提供了對窗口的管理的服務端實現(xiàn)劲室。
3伦仍、IWindowmanager.Stub.Proxy則是封裝了對Binder傳輸數(shù)據(jù)的實現(xiàn)结窘。

他們之間的關系可以這樣理解:
1很洋、?IWindowManagerImpl是客戶端類,它具備IWindowManager的接口隧枫,但其實它并不具備真正的管理窗口的能力喉磁。
2谓苟、所以IWindowManagerImpl最終會將消息轉發(fā)給WindowManagerService,但是因為WindowManagerService是遠程服務协怒,所以并不能直接將消息傳遞涝焙。
3、于是借助IWindowmanager.Stub.Proxy類孕暇,封裝了遠程的mRemote對象(實際就是WindowManagerService對象)并將對應的IWindowManager接口都實現(xiàn)數(shù)據(jù)傳輸接口仑撞,以便于數(shù)據(jù)能正在的發(fā)送給窗口管理服務WindowService

動態(tài)代理模式

所謂動態(tài)代理:即提供了在編譯時無法確定類型的代理方式妖滔,但無論怎么變它始終沒有脫離控制對象訪問的本質隧哮。

讓我們舉個例子來說明動態(tài)代理:我們在平時開發(fā)都會利用到接口,當后端同事為我們提供了豐富的 API 時座舍,每當多一個接口我們可能就要做很多事情沮翔。那么有沒有一種可能性,讓我們以成本最低的接入接口呢曲秉?

在繼續(xù)之前我們先舉個具象的例子采蚀,后端提供了我們“登錄”接口。
規(guī)定了以POST方式發(fā)起請求承二,需要傳入格式為 JSON 的數(shù)據(jù)榆鼠,同時需要包含兩個鍵名“username”、“password”亥鸠。

// 我們定義了如下的類:
@RestService
public interface ClerkAPI {

    @POST
    HealthbokResponse login(
            @Param("username") String target,
            @Param("password") String password
    );

我們使用@RestService標記類型璧眠,這顯然在后面用得著。用@POST標記請求方式读虏,用@Param標記傳入的參數(shù)责静,它們都只是普通的注解定義。

@Documented
@Target (TYPE)
@Retention (RUNTIME)
public @interface RestService {
}

這些信息也恰恰是后端同事告訴我們的僅有的信息盖桥,現(xiàn)在有個嚴格的要求是我們只利用這些信息灾螃。可以再不更改其它代碼的情況下完成對login()方法的調用揩徊。

public class RestServiceFactory {

    private static final ConcurrentMap<String, Object> serviceCaches = new ConcurrentHashMap<>();

    @SuppressWarnings("unchecked")
    public static <T> T getService(String baseUrl, Class<T> serviceClass) {
        T service;
        if (serviceClass.isAnnotationPresent(RestService.class)) {
            String key = serviceClass.getName();
            service = (T) serviceCaches.get(serviceClass.getName());
            if (service == null) {
                service = (T) Proxy.newProxyInstance(serviceClass.getClassLoader(), new Class[]{serviceClass}, new RestInvocationHandler(baseUrl));
                T found = (T) serviceCaches.putIfAbsent(key, service);
                if (found != null) {
                    service = found;
                }
            }
        } else {
            throw new IllegalArgumentException(serviceClass + " is not annotated with @RestService");
        }
        return service;
    }

    /**
     * Intercepts all calls to the the RestService Impl
     */
    @SuppressWarnings("unchecked")
    private static class RestInvocationHandler implements InvocationHandler {

        private String baseUrl;

        private RestInvocationHandler(String baseUrl) {
            this.baseUrl = baseUrl;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

            // 封裝請求信息
            HealthbokRequest request;
            // 真正的請求客戶端腰鬼,你可以將它理解為 HttpClient
            RestClient client = RestClient.getInstance();
            synchronized (client) {
                // 依據(jù)傳入的數(shù)據(jù),生成請求信息
                request = client.onPrepareRequest(baseUrl, method, args);
            }
            // 發(fā)起調用塑荒,返回值即是請求結果
            return client.call(request);
        }
    }
}

我們利用Proxy.newProxyInstance()動態(tài)的為接口創(chuàng)建了代理對象熄赡,以至于上層框架并不關心傳入的接口具體是哪個接口。它只要滿足@RestService的約束齿税,并符合@POST彼硫、@Param等一系列注解約束即可。

讓我們看下最后的調用方式,幾乎不用更改什么拧篮,除了傳入的@RestService 的 Class)以及對應的方法調用词渤。

RestServiceFactory
.getService("http://api.mock.com", ClerkAPI.class)
.login("1866824xxxx","24xxxx");

總結

嘮嘮叨叨寫了這么多沒有講太多理論性的東西,都是以實踐的方式記錄串绩。從分析 JAVA 缺虐、到 ANDROID的源碼分析,再到最后自己的API 接口開源項目片段摘取礁凡,哪里都有代理模式的身影高氮。

代理模式是用的非常普遍的模式,所以有必要從不同的視角去理解顷牌。但是萬變不離其宗纫溃,其本質無論如何都不會改變。變化的只是實現(xiàn)代理模式的過程(或是遠程通訊韧掩、或是動態(tài)創(chuàng)建)紊浩,所以多關注設計模式的本質才是重要的事情。


在整理過程中的一點復習資料:
1疗锐、Java 動態(tài)代理
2坊谁、grep 在線看源碼的小工具

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市滑臊,隨后出現(xiàn)的幾起案子口芍,更是在濱河造成了極大的恐慌,老刑警劉巖雇卷,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鬓椭,死亡現(xiàn)場離奇詭異,居然都是意外死亡关划,警方通過查閱死者的電腦和手機小染,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贮折,“玉大人裤翩,你說我怎么就攤上這事〉鏖” “怎么了踊赠?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長每庆。 經(jīng)常有香客問我筐带,道長,這世上最難降的妖魔是什么缤灵? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任伦籍,我火速辦了婚禮蓝晒,結果婚禮上,老公的妹妹穿的比我還像新娘鸽斟。我一直安慰自己拔创,他們只是感情好利诺,可當我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布富蓄。 她就那樣靜靜地躺著,像睡著了一般慢逾。 火紅的嫁衣襯著肌膚如雪立倍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天侣滩,我揣著相機與錄音口注,去河邊找鬼。 笑死君珠,一個胖子當著我的面吹牛寝志,可吹牛的內容都是我干的。 我是一名探鬼主播策添,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼材部,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了唯竹?” 一聲冷哼從身側響起乐导,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎浸颓,沒想到半個月后物臂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡产上,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年棵磷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晋涣。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡泽本,死狀恐怖,靈堂內的尸體忽然破棺而出姻僧,到底是詐尸還是另有隱情规丽,我是刑警寧澤,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布撇贺,位于F島的核電站赌莺,受9級特大地震影響,放射性物質發(fā)生泄漏松嘶。R本人自食惡果不足惜艘狭,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧巢音,春花似錦遵倦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至傲绣,卻和暖如春掠哥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背秃诵。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工续搀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人菠净。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓禁舷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親毅往。 傳聞我的和親對象是個殘疾皇子牵咙,可洞房花燭夜當晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn)煞抬,斷路器霜大,智...
    卡卡羅2017閱讀 134,654評論 18 139
  • 1:InputChannel提供函數(shù)創(chuàng)建底層的Pipe對象 2: 1)客戶端需要新建窗口 2)new ViewRo...
    自由人是工程師閱讀 5,301評論 0 18
  • Android跨進程通信IPC整體內容如下 1、Android跨進程通信IPC之1——Linux基礎2革答、Andro...
    隔壁老李頭閱讀 11,883評論 11 56
  • 毫不夸張地說战坤,Binder是Android系統(tǒng)中最重要的特性之一;正如其名“粘合劑”所喻残拐,它是系統(tǒng)間各個組件的橋梁...
    weishu閱讀 17,866評論 29 246
  • 代理模式是什么 如上圖所示途茫,代理代表著另一終端中的某個真實服務對象,Client 調用代理(Client help...
    野生西瓜閱讀 2,316評論 2 14