代理模式(控制对象访问)
提纲
最近在读 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
里看看到底发生了什么?
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
实现接口IWindowManager
而Stub
同时又继承自Binder
,Binder
具备远程通讯的能力。所以可以称Stub
是IWindowManager
接口实例的远程代理。
上图展示了接口IWindowManagerImpl
的继承结构,很容易联想到这是代理模式的实现。那我们看下这三个类之间的关系:
1、IWindowManagerImpl 是客户端窗口管理职责的实现类,它提供了窗口管理等一系列操作。
2、WindowManagerService
是android.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 在线看源码的小工具