代理模式:爱因斯坦和他的司机
🍐⚱️:(代理模式举例)爱因斯坦和他司机的故事——司机负责演讲,有人提问的时候就叫爱因斯坦起来回答问题 [1]。
小故事:爱因斯坦和他的司机
爱因斯坦提出相对论后,震惊世界,于是被很多大学邀请去做报告,爱因斯坦因此而被弄得疲惫不堪。
有一天,司机对他说:“你太累了,今天我帮你作报告吧?”
爱因斯坦问:“你能行吗?”
司机说:“我闭着眼睛都能背出来。”
那天司机上台,果然讲得滴水不漏。
但刚想下台时,一位博士站了起来,然后提了一个非常深奥刁钻的问题。
司机不知怎么作答,幸好脑瓜转得快:“你这问题太简单了,我司机都能回答。”
爱因斯坦站起来,几句话就解决了问题。
博士惊呆了:“没想到他的司机也远胜于我。”
但在回去的路上,司机对爱因斯坦说:“我知道的只是概念,你懂得的才是知识。”
代理模式可以在不改变原始类(被代理类)代码的情况下,通过引入代理类来给原始类附加功能。
登场角色:
Subject
(主体):Subject
角色定义了使Proxy
角色和RealSubject
角色之间具有一致性的接口。由于存在Subject
角色,所以Client
角色不必在意它所使用的究竟是Proxy
角色还是RealSubject
角色。Proxy
(代理人):Proxy
角色会尽量处理来自Client
角色的请求。只有当自己不能处理时,它才会将工作交给RealSubject
角色。Proxy
角色只有在必要时才会生成RealSubject
角色。Proxy
角色实现了在Subject
角色中定义的接口(API)。在示例程序中,由PrinterProxy
类扮演此角色。RealSubject
(实际的主体、被代理类):“本人”RealSubject
角色会在“代理人”Proxy
角色无法胜任工作时出场。它与Proxy
角色一样,也实现了在Subject
角色中定义的接口(API)。RealSubject
并不知道Proxy
的存在。Client
(请求者):使用Proxy
模式的角色。在 GoF 书中,Client
角色并不包含在Proxy
模式中。
代码示例:
1 | interface Subject { |
使用方式:
1 | public static void main(String[] args) { |
代理模式在访问实际对象时引入一定程度的间接性,利用这种间接性我们可以附加多种用途。拓展思路:
- 使用代理人来提升处理速度
- 我们可以不划分
Proxy
类和RealSubject
类,而是直接在RealSubject
类中加入惰性求值功能(即只有必要时才生成实例的功能)。不过划分Proxy
和RealSubject
角色可以使它们成为独立的组件,在修改的时候也不会互相之间产生影响(分而治之)。 - 代理与委托。代理人只代理他能解决的问题。当遇到他不能解决的问题时,还是会「委托」给本人去解决。(在现实世界中,应当是本人将事情委托给代理人负责,而在设计模式中则是反过来的。)
- 透明性。在
Main
中Client
直接使用Subject
接口,Main
不必在意调用的究竟是哪个类。在这种情况下,可以说Proxy
类是具有「透明性」的。 - 各种代理模式:代理模式有很多种变化形式。
- Virtual Proxy(虚拟代理):Virtual Proxy 就是本章节演示的代理模式。只有当真正需要实例时,它才生成和初始化实例。
- Remote Proxy(远程代理):Remote Proxy 可以让我们完全不必在意 RealSubject 角色是否在远程网络上,可以如同它在自己身边一样(透明性地)调用它的方法。Java 的 RMI (Remote Method Invocation 远程方法调用) 就相当于 Remote Proxy。或者 RPC 框架。
- Access Proxy:Access Proxy 用于在调用 RealSubject 角色的功能时设置访问限制。例如,这种代理可以只允许指定的用户调用方法,而当其他用户调用方法时则报错。
打印机案例
Printer
打印机print()
函数调用了heavyJob
函数。PrinterProxy
不实现真正的print
函数。realize
函数用于实现真正的打印机,这个函数只在调用了代理类的print
函数时调用。不论setPrinterName
方法和getPrinterName
方法被调用多少次,都不会生成Printer
类的实例。只有当真正需要本人时,才会生成Printer
类的实例。- 上图疑似有误:
Client
应当使用抽象Subject
才对
相关的设计模式:
- 适配器模式 Adapter:Adapter 模式适配了两种具有不同接口(API)的对象,以使它们可以一同工作。而在 Proxy 模式中,
Proxy
角色与RealSubject
角色的接口(API)是相同的(透明性)。 - 装饰者模式 Decorator:Decorator 模式与 Proxy 模式在实现上很相似,不过它们的使用目的不同。Decorator 模式的目的在于增加新的功能。而在 Proxy 模式中,与增加新功能相比,它更注重通过设置代理人的方式来减轻本人的工作负担。
通过继承的方式代理第三方类
在上面的例子中,代理类 Proxy
和被代理类 RealSubject
都继承同一个接口。如果被代理类没有定义接口,且无法修改源码(如第三方类库的类),我们就不能用上面的方法实现代理模式。
解决方案:继承。让代理类继承原始类,然后扩展附加功能。
代理类 Proxy
通过 super.request1()
的方式委托被代理类。
动态代理 Dynamic Proxy
由程序员创建或特定工具自动生成代理模式的源代码,即在编译前就已经将接口,被代理类,代理类等确定下来的代理方式,叫做静态代理。
如果通过静态代理的方式实现代理模式,对于每个代理类 Proxy
,我们都需要将被代理类的所有方法重新实现一遍,且代理类 Proxy
每个方法都有相似的代码逻辑。如果被代理类很多,那么代理类也会很多,增加代码的维护成本。
动态代理:不事先为每个原始类编写代理类,而是在运行的时候,根据我们在 Java 代码中的「指示」,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。
代理方式 | 静态代理 | 动态代理 |
---|---|---|
JVM | 编译时生成代理类的 class 文件 | 运行时动态生成类字节码并加载到 JVM |
灵活性 | 🔴接口一旦新增加方法,目标对象和代理对象都要进行修改 | 🟢不需要针对每个目标类都创建一个代理类 |
Java 标准库提供了动态代理机制,可以在运行期动态创建某个 interface
的实例,不用编写实现类。动态代理依赖 Java 站内文章反射 语法。
Java 常用的动态代理实现方式有:
动态代理 | JDK | CGLIB |
---|---|---|
代理对象 | 实现了接口的类或者直接代理接口 | 未实现任何接口的类 |
效率 | 🟢 | 🟡 |
特点 | 简单易用 | 引入第三方库 |
在 Spring 中的 站内文章AOP 模块中,如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
JDK 动态代理
假设,代理类 Proxy
在调用被代理类前都需要执行前置方法和后置方法。在静态代理方式,我们需要将 Proxy
的 request1
、request2
、request3
都改动代码。
1 | public class Proxy implements Subject{ |
在 Java 的 java.lang.reflect
包下提供了一个 Proxy
类和一个 InvocationHandler
接口,通过这个类和这个接口可以生成 JDK 动态代理类和动态代理对象。
JDK 动态代理类使用步骤:
- 定义一个接口及其实现类;
- 自定义
InvocationHandler
并重写invoke
方法,在invoke
方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑; - 通过
Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
方法创建代理对象;
下面是示例代码:
1 | class MyInvocationHandler<T> implements InvocationHandler { |
主函数使用动态代理:
1 | public class ProxyTest { |
当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现 InvocationHandler
接口类的 invoke
方法来调用。
此处动态代理的优势体现为:可以方便对代理类的函数进行统一处理,不用修改代理类中的方法。因为代理类中的方法,是通过 InvocationHandler
中的 invoke
方法调用的,所以我们只要在 invoke
方法中统一处理,就可以对所有被代理的方法进行相同的操作了。
优化:使用工厂创建代理类
在上面的 main
代码调用中,我们创建实现了 Subject
接口的代理类还能通过 站内文章引入工厂 进一步优化。
1 | class SubjectProxyFactory { |
main
代码的调用简化为:
1 | Subject subject = new RealSubject(); |
源码浅析(真的很浅)
通读 JDK 动态代理实现源码,我们可以简单理解为 JVM 帮我们自动编写了一个下面的类。这个类时动态生成的。
1 | // 这个代码是对源码的极大压缩和简化,实际的代理类生成很复杂 |
上面 JVM 动态生成的代码中 implements Subject
注定了 JDK 动态代理方式只能代理接口,而不能代理类。
CGLIB 动态代理
CGLIB(Code Generation Library)是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。
在 CGLIB 动态代理机制中 MethodInterceptor
接口和 Enhancer
类是核心。
CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final
类型的类和方法,private
方法也无法代理。
1 | // 被代理类 |
代理模式的应用
与业务不太相关的附加功能
业务系统中存在一些附加功能,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们可以将将这些附加功能与业务功能解耦,放到代理类中统一处理,让程序员只需要关注业务方面的开发。在 Spring 中,这部分的工作可以交给 AOP 完成。
RPC 框架
详看文章:站内文章简易 RPC 调用框架的实现
缓存
假设我们要开发一个接口请求的缓存功能,对于某些接口请求,如果入参相同,在设定的过期时间内,直接返回缓存结果,而不用重新进行逻辑处理。比如,针对获取用户个人信息的需求,我们可以开发两个接口,一个支持缓存,一个支持实时查询。对于需要实时数据的需求,我们让其调用实时查询接口,对于不需要实时数据的需求,我们让其调用支持缓存的接口。
参考实现方式:在应用启动的时候,我们从配置文件中加载需要支持缓存的接口,以及相应的缓存策略(比如过期时间)等。当请求到来的时候,我们在 AOP 切面中拦截请求,如果请求中带有支持缓存的字段(比如 http://…?..&cached=true
),我们便从缓存(内存缓存或者 Redis 缓存等)中获取数据直接返回。
本文 PlantUML 归档
1 | interface Subject{ |
1 | class RealSubject{ |