前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >代理模式(控制对象访问)

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

作者头像
幺鹿
发布2018-08-21 15:48:23
4720
发布2018-08-21 15:48:23
举报
文章被收录于专栏:Java呓语Java呓语

提纲

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

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

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

普通代理模式

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

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

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

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

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

代码语言:javascript
复制
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()方法。

代码语言:javascript
复制
abstract class FileSystem {
        public abstract boolean delete(File f);
}

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

FileSystem的子类们

代码语言:javascript
复制
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为例:

代码语言:javascript
复制
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”。

代码语言:javascript
复制
// 我们定义了如下的类:
@RestService
public interface ClerkAPI {

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

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

代码语言:javascript
复制
@Documented
@Target (TYPE)
@Retention (RUNTIME)
public @interface RestService {
}

这些信息也恰恰是后端同事告诉我们的仅有的信息,现在有个严格的要求是我们只利用这些信息。可以再不更改其它代码的情况下完成对login()方法的调用。

代码语言:javascript
复制
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)以及对应的方法调用。

代码语言:javascript
复制
RestServiceFactory
.getService("http://api.mock.com", ClerkAPI.class)
.login("1866824xxxx","24xxxx");

总结

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

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

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.03.11 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 提纲
  • 普通代理模式
  • 远程代理模式
  • 动态代理模式
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档