代理模式(控制对象访问)

提纲

最近在读 Android Binder 部分的源码,之前三三两两的读过一些片段。但总是感觉理解的不深刻,在读源码的过程中看到了代理模式的应用,那便把代理模式单独开一章描述清楚,需要查看其它设计模式描述可以查看我的文章《设计模式开篇》

本篇文章将根据以下知识点展开描述:

1、普通代理模式(分析 Java 文件操作源码)
2、远程代理模式(分析 Android Binder Service 源码)
3、动态代理实现(分析 API 模块设计)

普通代理模式

使用java.io.File来形容代理模式的本质是再恰当不过的事情了,为了保证上下文的连贯性,请容许我设计一个文件操作的场景。

假使你需要使用批复同事转发给你的文件,你使用程序读取出文件内容,等你阅读完毕后你会往文件中加入你的意见。在批复完成后,你会将文件通过邮件回复给同事,并同事删除本地的备份。

在动工之前假设你会考虑如下情景:

  • 文件是否为空
  • 是否有权限读取文件
  • 是否有权限写入文件
  • 删除文件

文件操作 JDK 已经为我们内置好了自然不用我们重复开发轮子,让我们看看这部分的代码。

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

我们发现java.io.File这个类并没有真正的涉及到文件的操作,而只是对真正的操作的一层包装。比如每个方法中都使用了SecurityManager做安全检测,而在检测通过时又都使用FileSystem的实例fs调用到真正的实现。

FileSystem是抽象类,它定义了所有File类会调用到的底层的实现,比如下面的 delete()方法。

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

我们来跟踪下FileSystem的子类,显示它支持了 Unix 与 Window 两种文件系统。让我们跟进到UnixFileSystem里看看到底发生了什么?

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方法完成了对文件的删除操作。

分析到这里我们发现了上层的File文件实际上并没有完成任何的文件的操作,而只是对FileSystem的封装调用+权限检查。如果你仔细阅读我贴出的代码,你会发现FileSystem类本身或其子类的访问权限都是包访问权限,而这恰恰佐证了代理模式的本质——控制对象访问。

代理模式的本质:控制对象访问。

具有控制对象访问思想特征设计模式有很多种,比如:中介、门面,甚至单例都具备该特征,代理模式在某种程度而言比其它表现方式更纯粹。

远程代理模式

在有了普通代理模式的基础,我们接下去分析说明是远程代理模式。其实远程代理与普通代理的差距很小, 以 `File``作为例子,普通代理模式的调用图如下:

普通代理模式

而远程代理模式与普通代理模式的区别是:有别于普通代理模式的本地调用转发,远程代理模式使用 远程协议 描述了 File --> FileSystem 的转发过程。

很好的参考例子是 Android 的 Binder 部分,我们这里将贴出部分的相关代码。不知是否是为了区分远程代理与普通代理,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实现接口IWindowManagerStub同时又继承自BinderBinder具备远程通讯的能力。所以可以称StubIWindowManager接口实例的远程代理。

远程代理模式

上图展示了接口IWindowManagerImpl的继承结构,很容易联想到这是代理模式的实现。那我们看下这三个类之间的关系:
1、IWindowManagerImpl 是客户端窗口管理职责的实现类,它提供了窗口管理等一系列操作。
2、WindowManagerServiceandroid.view.IWindowManager.Stub的实现类,它提供了对窗口的管理的服务端实现。
3、IWindowmanager.Stub.Proxy则是封装了对Binder传输数据的实现。

他们之间的关系可以这样理解:
1、�IWindowManagerImpl是客户端类,它具备IWindowManager的接口,但其实它并不具备真正的管理窗口的能力。
2、所以IWindowManagerImpl最终会将消息转发给WindowManagerService,但是因为WindowManagerService是远程服务,所以并不能直接将消息传递。
3、于是借助IWindowmanager.Stub.Proxy类,封装了远程的mRemote对象(实际就是WindowManagerService对象)并将对应的IWindowManager接口都实现数据传输接口,以便于数据能正在的发送给窗口管理服务WindowService

动态代理模式

所谓动态代理:即提供了在编译时无法确定类型的代理方式,但无论怎么变它始终没有脱离控制对象访问的本质。

让我们举个例子来说明动态代理:我们在平时开发都会利用到接口,当后端同事为我们提供了丰富的 API 时,每当多一个接口我们可能就要做很多事情。那么有没有一种可能性,让我们以成本最低的接入接口呢?

在继续之前我们先举个具象的例子,后端提供了我们“登录”接口。
规定了以POST方式发起请求,需要传入格式为 JSON 的数据,同时需要包含两个键名“username”、“password”。

// 我们定义了如下的类:
@RestService
public interface ClerkAPI {

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

我们使用@RestService标记类型,这显然在后面用得着。用@POST标记请求方式,用@Param标记传入的参数,它们都只是普通的注解定义。

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

这些信息也恰恰是后端同事告诉我们的仅有的信息,现在有个严格的要求是我们只利用这些信息。可以再不更改其它代码的情况下完成对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) {
                // 依据传入的数据,生成请求信息
                request = client.onPrepareRequest(baseUrl, method, args);
            }
            // 发起调用,返回值即是请求结果
            return client.call(request);
        }
    }
}

我们利用Proxy.newProxyInstance()动态的为接口创建了代理对象,以至于上层框架并不关心传入的接口具体是哪个接口。它只要满足@RestService的约束,并符合@POST@Param等一系列注解约束即可。

让我们看下最后的调用方式,几乎不用更改什么,除了传入的@RestService 的 Class)以及对应的方法调用。

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

总结

唠唠叨叨写了这么多没有讲太多理论性的东西,都是以实践的方式记录。从分析 JAVA 、到 ANDROID的源码分析,再到最后自己的API 接口开源项目片段摘取,哪里都有代理模式的身影。

代理模式是用的非常普遍的模式,所以有必要从不同的视角去理解。但是万变不离其宗,其本质无论如何都不会改变。变化的只是实现代理模式的过程(或是远程通讯、或是动态创建),所以多关注设计模式的本质才是重要的事情。


在整理过程中的一点复习资料:
1、Java 动态代理
2、grep 在线看源码的小工具

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,780评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,424评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,397评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,576评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,997评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,945评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,107评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,850评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,625评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,804评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,285评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,613评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,291评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,164评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,963评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,096评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,886评论 2 278

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,110评论 18 139
  • 1:InputChannel提供函数创建底层的Pipe对象 2: 1)客户端需要新建窗口 2)new ViewRo...
    自由人是工程师阅读 5,103评论 0 18
  • Android跨进程通信IPC整体内容如下 1、Android跨进程通信IPC之1——Linux基础2、Andro...
    隔壁老李头阅读 11,483评论 11 56
  • 毫不夸张地说,Binder是Android系统中最重要的特性之一;正如其名“粘合剂”所喻,它是系统间各个组件的桥梁...
    weishu阅读 17,554评论 29 246
  • 代理模式是什么 如上图所示,代理代表着另一终端中的某个真实服务对象,Client 调用代理(Client help...
    野生西瓜阅读 2,239评论 2 14