设计模式设计模式

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

2017-03-11  本文已影响88人  幺鹿

提纲

最近在读 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 在线看源码的小工具

上一篇下一篇

猜你喜欢

热点阅读