代理模式的原理非常简单,它和装饰模式很类似,都是在不改变同一个接口功能的前提下,对原有接口功能做扩展。但是代理模式的应用却比装饰模式更为广泛,因为代理模式并不执着于链式结构,而是采用更为灵活的单一结构,在很多框架和组件的设计里都能看到代理模式的身影,比如,JDK 的动态代理机制、Spring 的 AOP 机制、Dubbo 框架等。

那么,为什么代理模式能够获得如此广泛的应用呢?下面我们一起来看看。

一、模式原理分析

代理模式的原始定义是:让你能够提供对象的替代品或其占位符。代理控制着对于原对象的访问,并允许将请求提交给对象前后进行一些处理。

从这个定义中我们能看出,代理模式是作为对象之间的一种中间结构来使用的,通过构建一个代理对象来对原始的功能进行委托处理,其中有一个很重要的功能就是控制对象的访问。拿现实中的例子来说,假设你有一套房子要卖,可你却正好在外地出差,你不能亲自处理带人看房、过户等购房手续,这时你就可以找一个房产中介来作为你的代理人,委托他来帮你带人看房、处理过户手续。这就是现实中经典的代理模式的例子。我们先来看看代理模式的 UML 图:

从这个 UML 图中,我们能看出代理模式的三个关键角色。

  • 抽象主题类(RealObject):声明公用的方法,定义可供客户端使用的统一功能。

  • 主题实现类(RealObjectImpl):实现了抽象主题类的所有方法。

  • 代理类(Proxy):实现了抽象主题类的方法,并隐藏在代理后面可能其他类的实现。

在图中这三个角色都有相互依赖的关系,代理类采用继承的方式来获取抽象主题类的公共方法定义,在代理类内可以进行相关的扩展操作,但最终还是需要执行主题实现类的方法。这是和适配器模式很不同的地方,适配器模式是转换为新的接口,而代理模式不会改变原有接口。

接下来,我们再来看看该 UML 对应的代码实现:

public interface RealObject {
    void doSomething();
}
public class RealObjectImpl implements RealObject {
    @Override
    public void doSomething() {
        System.out.println("=== 真实对象输出打印");
    }
}
public class Proxy extends RealObjectImpl {
    @Override
    public void doSomething() {
        //这里做一些代理操作或额外的操作
        System.out.println("== 通过代理类来执行真实对象");
        super.doSomething();
    }
}
//单元测试
public class Demo {
    public static void main(String[] args) {
        RealObject realObjectProxy = new Proxy();
        realObjectProxy.doSomething();
    }
}
输出:
== 通过代理类来执行真实对象
=== 真实对象输出打印

在上面的这段代码实现中,RealObjectImpl 实现了接口 RealObject 的功能 doSomething,这时我们又创建了一个代理对象 Proxy,它继承了 RealObjectImpl,目的是在使用 RealObject 时可以做一些额外的操作。

代码实现虽然简单,但是却展现了代理模式的基本理念——作为一个外包装的中间层,享有控制住访问对象的权利,同时也能扩展一些功能

二、为什么要使用代理模式

虽然代理模式的基本原理非常简单,但是你可能会问:为什么不直接使用已有对象的功能,而非要在中间加一层代理呢?其实主要有三个原因。

第一个,客户端有时无法直接操作某些对象。 比如,在分布式应用中,你需要调用的对象可能是运行在另外一台服务器上的,当你访问它时,就必须要通过网络才能访问。如果你让客户端直接去调用,那么就意味着客户端需要处理网络服务,包括连接、打包、传包、解包等复杂操作;而这时如果你使用代理模式,在客户端和远程服务端之间建立一个网络代理对象,那么客户端只需要调用代理对象就能跟远程对象建立联系,甚至就像调用本地对象一样。这其实就是我们常说的 RPC 服务的基本原理,本质上就是代理模式。

第二个,客户端执行某些耗时操作容易造成服务端阻塞。 比如,在类似有道、石墨、语雀这样的云编辑器里进行文案编写时,拷贝多张图片可能就是一件非常耗时的操作,使用者并不希望在执行拷贝图片的操作后,打字就无法正常操作甚至无法查看其他页面。这时,对于软件设计者来说,图片的加载就可以通过代理模式来解决:标示图片所在位置,然后使用代理对象去读取图片资源,这样就不会影响其他客户端与服务端之间的操作了。

第三个,服务端需要控制客户端的访问权限。 代理模式除了前面提到的扩展功能外,另一个更为重要的功能是做权限控制。比如,某一项业务由于安全原因只能让一部分特定的用户去访问,如果在原有功能的基础上再增加权限过滤功能就会增加代码的耦合性,并且也不方便组件的复用。其实,这时做一个代理类就可以解决该问题,对于特定的接口来说,只需要指定所有请求必须通过该代理类,然后由该代理类做权限判断即可。

三、使用场景分析

实际上,代理模式应用非常广泛,目前的使用场景大致可总结为以下五大类。

第一类,虚拟代理,适用于延迟初始化,用小对象表示大对象的场景。这个“大对象”会包含大量 IO 资源,比如图片、大文件、模型文件等。我们都知道,大对象通常很占用内存空间,一直保持其运行会很消耗系统资源,这时就可以使用代理模式。那怎么来做呢?可以先创建一个消耗相对较小的对象来代理这个大对象的创建,而实际上真实的大对象只会在真正需要时才会被创建,这样的代理方式就被称为虚拟代理。比如,在 Java 中的 CopyOnWriteArrayList 数组对象的实现就是使用了虚拟代理的方式,目的就是要让操作延迟,只有对象被真正用到的时候才会被克隆。

第二类,保护代理,适用于服务端对客户端的访问控制场景。代理模式有一个非常重要的应用场景就是控制一个对象对另一个对象的访问与使用权限。当客户端通过代理对象访问服务端的原始对象时,代理对象会根据具体的规则来判断客户端是否有访问权限。比如,防火墙其实就是一种保护代理的具体实践。

第三类,远程代理,适用于需要本地执行远程服务代码的场景。 在这种场景中,代理对象会隐藏处理所有与网络相关的复杂细节。随着微服务架构的流行,越来越多的程序应用部署在多台服务器上,各自服务都更专注于各自的业务,当需要使用其他服务时就会频繁进行远程服务调用,但不可能所有的业务都要自己实现网络调用,于是就出现了的远程代理框架,比如,gRpc、Dubbo 等。

第四类,日志记录代理,适用于需要保存请求对象历史记录的场景,比如,日志监控。客户端在调用请求时,并不会感知到日志记录,这是因为代理对象在原始对象周围添加了监控功能。

第五类,缓存代理,适用于缓存客户请求结果并对缓存生命周期进行管理的场景。比如,商品详情页通常包含大量图片和文字介绍,代理对象可以对重复请求相同的结果进行缓存。

四、代理模式有什么优缺点

从上面的使用场景分析中,我们可以总结提炼出代理模式的优点和缺点。

其中,代理模式的优点主要有以下。

  • 作为接口的特定中间层,能够降低对象间的直接耦合。代理对象很好地解耦了客户端与服务端之间的调用关系,即使客户端在使用服务端对象还未准备好或不存在时,代理也可以正常工作。

  • 虚拟代理通过延迟加载以及使用小对象代表大对象的方式,帮助减少系统资源的损耗,提升系统运行速度。

  • 保护代理可以控制客户端对服务端的访问权限。

  • 远程代理帮助客户端快速访问分布式机器上的对象,分布式服务器通常可以提供集群负载均衡、故障容错和高性能的计算能力。

  • 日志记录代理能记录每次操作的信息,对于用户使用轨迹跟踪、数据统计、定位问题等都有重要作用。

  • 缓存代理能够提供各式各样的缓存结果,对于需要高频读取重复数据的系统来说,能极大地提升系统性能。

而代理模式的缺点有如下几个。

  • 因为在客户端和服务端之间增加了代理对象,所以也增加了系统的复杂度。

  • 实现了代理模式的服务,如果处理请求的时间过长,就容易造成多个服务调用阻塞而影响整体系统的处理速度。

  • 对于一些偏操作系统或标准协议等底层的代理服务而言,代码实现可能很复杂。

文章(专栏)将持续更新,欢迎关注公众号:服务端技术精选。欢迎点赞、关注、转发