Design-Pattern-Proxy
代理模式是一种结构型设计模式,通过引入代理对象来控制对原始对象的访问,常用于功能扩展、访问控制或性能优化。
场景
在编写代码时,经常会出现需要在方法开始或结尾,编写固定格式代码的情况。例如在接口层方法中,需要对入参和出参进行日志打印;又或者是在业务逻辑方法中,执行前需要开启事务,结束时需要提交/回滚事务。
我们当然可以在这些方法中显式编码日志输出,也能正确执行。但当日志输出需要调整时,需要对所有编码的位置改动。日志输出本身也不属于业务的一部分,写在一起会导致两个不相关功能耦合。
理想状态是,能把这些通用日志代码写在统一位置,然后在每个需要的地方,通过某种标记,使得程序能完成两部分代码功能的组合。这个场景和之前提到的装饰模式十分类似。在装饰模式中通过包装,允许在不修改现有对象结构的前提下向对象添加新的功能。而代理模式,也是在原始对象外部,通过包装来实现在调用原始对象的方法前,新增额外的逻辑。
1 |
|
上例中通过包装,ProxyXxxService
通过重写了doService
同名方法,在XxxService
的doService
方法执行前后增加了额外逻辑。但还是存在问题,为了使代理生效,我们需要将所有用到被代理对象的地方,替换为代理对象。java
是强类型语言,上例中代理对象和被代理对象属于不同类型,直接替换会因为类型不匹配无法编译。为了使代理对象能够赋值给原先被代理对象的变量,需要两者实现共同接口,并且定义变量类型为接口,或者代理类是被代理类的子类。客户端不知道也无需关心,其持有的对象是否被代理,好一招狸猫换太子。
1 |
|
代理模式
在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
常用的动态代理技术有两种,JDK
和CGLIB
。对此两种具体实现和使用本文不过多介绍。值得一提的是,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
在动态代理基础上又进行了封装,使得程序员可以声明式定义切面逻辑,无需重复编写绑定代码。