Design-Pattern-Proxy

代理模式是一种结构型设计模式,通过引入代理对象来控制对原始对象的访问,常用于功能扩展、访问控制或性能优化。

场景

在编写代码时,经常会出现需要在方法开始或结尾,编写固定格式代码的情况。例如在接口层方法中,需要对入参和出参进行日志打印;又或者是在业务逻辑方法中,执行前需要开启事务,结束时需要提交/回滚事务。

我们当然可以在这些方法中显式编码日志输出,也能正确执行。但当日志输出需要调整时,需要对所有编码的位置改动。日志输出本身也不属于业务的一部分,写在一起会导致两个不相关功能耦合。

理想状态是,能把这些通用日志代码写在统一位置,然后在每个需要的地方,通过某种标记,使得程序能完成两部分代码功能的组合。这个场景和之前提到的装饰模式十分类似。在装饰模式中通过包装,允许在不修改现有对象结构的前提下向对象添加新的功能。而代理模式,也是在原始对象外部,通过包装来实现在调用原始对象的方法前,新增额外的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class XxxService {
public void doService {
...
}
}

public class ProxyXxxService {
private XxxService xxxService;
public void doService {
beforeFunction();
xxxService.doService();
afterFunction();
}
}

上例中通过包装,ProxyXxxService通过重写了doService同名方法,在XxxServicedoService方法执行前后增加了额外逻辑。但还是存在问题,为了使代理生效,我们需要将所有用到被代理对象的地方,替换为代理对象。java是强类型语言,上例中代理对象和被代理对象属于不同类型,直接替换会因为类型不匹配无法编译。为了使代理对象能够赋值给原先被代理对象的变量,需要两者实现共同接口,并且定义变量类型为接口,或者代理类是被代理类的子类客户端不知道也无需关心,其持有的对象是否被代理,好一招狸猫换太子。

1
2
3
4
5
6
7
8
9
10
public class XxxService implements AInterface
public class ProxyXxxService implements AInterface

public class YyyService
public class ProxyYyyService extends YyyService

public class client {
private AInterface xxxService;
private YyyService yyyService;
}

代理模式

GoF书中,代理模式专指通过接口实现的模式。根据书中定义,代理模式共有四个角色:

  • 服务接口 (Service Interface) 声明了服务接口。 代理必须遵循该接口才能伪装成服务对象。
  • 服务 (Service) 类提供了一些实用的业务逻辑。
  • 代理 (Proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。
  • 客户端 (Client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。
classDiagram
    direction LR
    class Client
    
    class ServiceInterface {
        << interface >>
    	+methodA() ResultA
    }
	Client --> ServiceInterface
	
	class Service {
		+methodA() ResultA
	}
    Service ..|> ServiceInterface
	
	class Proxy {
		- ServiceInterface serviceInterface
		+methodA() ResultA
	}
	Proxy ..|> ServiceInterface
	Proxy o-- Service
    

动态代理

但任有问题。如果需要将日志的功能,通过代理加入到多个类中,会额外产生和被代理类数量相等的代理类。这些代理类关于日志的代码仍然高度重合,代理模式没有降低代码的重复程度。为了解决这个问题,不同语言有不同做法。在java中,通常使用动态代理技术来完成前文提到的“两部分代码功能的组合”。动态代理模式下,代理对象是程序在运行时动态生成字节码,将日志功能增强方法和被代理对象,组合成的临时类对象。也就是说这动态代理中,这个代理类并没有在代码中显式编码,而只需要编写增强目标方法的核心方法(简称增强方法)。程序员可以独立地编写增强方法和被代理类,然后在任何需要被代理的对象上绑定增强方法,最后由动态代理完成两者的组合。

java常用的动态代理技术有两种,JDKCGLIB。对此两种具体实现和使用本文不过多介绍。值得一提的是,JDK要求被代理类实现了某个接口,CGLIB要求被代理类/方法不能是final,结合上文也是好理解的。只有被代理类实现了某个接口,客户端才可以无缝替换被代理对象。而通过继承实现的代理,要求父类不能是final,方法也不能是final,不然无法继承或重写父类定义的方法。

Spring AOP

现代基于Java开发,本质上已经是基于Spring开发。而Spring AOP又对动态代理进行了封装,使得程序员能够更轻松地使用代理模式。

在没有Spring AOP时,动态代理仍需要手动显示编写将代理方法和被代理对象绑定到一起。每次代理新类时,需重复编写类似的绑定代码。

Spring AOP提供了更为简单的绑定方式。在Spring AOP术语中,结合上文不严谨地说,通知(Advice)就是增强方法,连接点(Joint Point)/切点(Point Cut)就是需要被代理的方法/类,把增强方法和被代理类组合到一起的过程称为织入(Weaving。可以通过注解(如@Aspect)或配置声明式定义切面逻辑,Spring自动完成代理对象的创建和绑定,开发者无需手动组合增强方法与被代理对象。

所以在Spring IoC容器中,会同时存在原始对象增强后的代理对象。依赖注入会替代原始对象,将代理对象注入到依赖的地方。所有依赖处的调用,实际是调用了代理对象增强后的方法。这也解释了为什么在被代理类中,如果通过this.method()的方式调用同类中被增强的方法会失效。因为此时的this指的是原始对象,而只有通过代理对象调用方法,才会执行增强逻辑。

一些想法

在学习完代理模式的结构后,会发现代理模式是一种非常简单的设计模式。本质就是通过包装,在被代理方法执行前后/抛异常时增加一些额外逻辑。而每次阅读到其他代理模式的文章,都会使用“控制对原始对象的访问”这类难理解的定义。在笔者理解中这是容易产生歧义的地方。控制访问容易被理解为,客户端要调用某些方法,需要进行权限校验。这类鉴权的逻辑和后续执行的业务逻辑通常是独立的,十分适合通过代理模式进行解耦。但从上下文推断,笔者认为可能更想表达,客户端无法直接接触原方法,欲访问原方法,必先通过代理方法,这一层控制的含义。

提到代理就涉及三方角色,原业务的双方和代理人。在现实中我们讨论代理,是指代理人可以以被代理人名义,在授权范围内与第三方实施民事法律行为,法律后果由被代理人承担,被代理人通常是业务发起方。而在上文中,被代理的角色是服务,是被调用方,是业务应答方。沿用计算机网络中的概念,现实中讨论的代理更多是正向代理,而代理模式则是反向代理。这导致代理模式某些地方可能和直观不符。

和其他包装的区别

在前些节提到,适配器模式代理模式桥接模式装饰模式均属于包装

适配器模式会改变包装前后实现的接口,桥接模式中抽象和实现没有实现相同接口,不可互相替代,因此容易区分。

而装饰模式中,具体部件和具体装饰都实现了部件接口,装饰的嵌套也动态增强了部件的功能。对比代理模式中,代理类实现了被代理类的接口,也通过增强方法动态增强了被代理对象的功能。代理本身也可以嵌套,即代理对象被再次代理,形成“代理链”。从结构和形式来看,代理模式和装饰模式几乎没有区别

翻阅了一些网上文章,给出的区别并不能很好地说服笔者。

  • 有些人认为代理模式对于被包装的对象,负责其创建,而装饰模式的被包装对象,则更多是外部传入。但代理模式也不是必须管理被代理对象的生命周期,这显然不是区别两个模式的核心特征。
  • 也有人认为,两种模式的主要区别是目的动机不同,认为代理模式强调控制访问,装饰模式强调动态增强功能。笔者仍然不认可,螺丝刀可以用来拧螺丝,也可以拿来开快递。我们不能说用来开快递的时候,它就不是螺丝刀。而且很多代理模式的案例,相较于控制访问,更多是在进行动态增强。例如利用代理模式打印日志,就是把日志功能动态增强到被代理类中。退一步讲,访问控制算不算功能增强。
  • 也有说装饰模式是运行时行为,代理模式是编译时就能确定的。但如果代理模式是通过运行时读取配置,再决定是否生成代理对象,如此也成为了运行时行为。

会有这些讨论,部分原因是代理模式在Spring Aop的发展下,已经和原来的代理模式有了比较大区别。在讨论代理模式和装饰模式的区别时,很多时候思考的是Spring Aop和装饰模式的区别。原始的代理模式中,代理类和被代理类是一对一关系,也没有将增强方法从代理类中解耦,再用动态代理绑定的模式。当考虑Spring Aop的案例时,已经脱离了控制访问的范畴,更多场景就是用来做动态增强功能。

所以笔者认为两者的主要区别,是代理模式的增强方法,和原方法的业务关联性不高。而装饰模式的装饰方法,则和被装饰的类高度相关。正是因为代理模式的增强方法更通用,代理模式才可以发展成只要编写增强方法,然后绑定到各种需要的地方。而装饰模式设计初衷是针对某一具体功能,没有通用性。

总结

代理模式通过实现和被代理对象相同接口,或继承自被代理类,使得可以在客户端中替换被代理对象。具体是通过包装,并重写被代理对象的同名方法,在委派调用被代理对象方法前后增加额外逻辑来实现。

GoF书中的代理模式特指通过实现共同接口的模式。此代理模式包含四个角色:服务接口、服务、代理、客户端。

为了解决代理模式中代理和服务一一对应的关系导致的代码重复,java使用动态代理技术,进一步降低增强方法和被代理方法之间的耦合。

Spring AOP在动态代理基础上又进行了封装,使得程序员可以声明式定义切面逻辑,无需重复编写绑定代码。


Design-Pattern-Proxy
http://dracoyus.github.io/2025/02/27/Design-Pattern-Proxy/
作者
DracoYu
发布于
2025年2月27日
许可协议