代理模式
代理模式
Rif设计模式笔记:代理模式 (Proxy Pattern)
一、一句话概括
为另一个对象提供一个替身或占位符,以控制对这个真实对象的访问。这个替身(代理)可以在将请求转发给真实对象的前后,执行一些附加操作。
二、为什么需要它 (Why - The Pain Point)
在很多情况下,我们不希望或不能直接访问一个对象。原因可能有很多:
- 访问控制:我们希望根据用户的权限,决定他是否能调用对象的某些方法。例如,普通用户只能读取数据,而管理员可以修改数据。
- 延迟加载(懒加载):一个对象的创建成本非常高昂(比如,加载一张高清大图或初始化一个复杂的数据库连接)。我们希望只有在真正需要它的时候才去创建,而不是在一开始就加载。
- 远程访问:真实对象位于远程服务器上,客户端需要通过网络来调用它的方法。直接进行网络通信非常复杂。
- 日志记录/性能监控:我们想在不修改原始代码的情况下,记录下对象方法的调用时间、参数、返回值等信息。
- 缓存:对于一些计算成本高但结果不常变的方法,我们希望将结果缓存起来,下次同样的请求直接返回缓存结果,而不是重新计算。
核心问题:如何在不改变原始对象(或无法改变它)的前提下,为其访问过程增加额外的控制逻辑?
三、它是什么 (What - The Solution)
代理模式通过引入一个代理对象(Proxy Object) 来解决这个问题。这个代理对象和真实对象实现了同一个接口,因此对于客户端来说,代理对象和真实对象是完全一样的,可以互换使用。
核心思想和角色:
- **主体接口 (Subject)**:
- 定义了真实对象(Real Subject)和代理对象(Proxy)共同的接口。
- 这样,任何可以使用真实对象的地方,也都可以使用代理对象。
- **真实主体 (Real Subject)**:
- 实现了
Subject接口,是真正执行业务逻辑的那个对象。 - 代理对象最终会将请求转发给它。
- 实现了
- **代理 (Proxy)**:
- 也实现了
Subject接口。 - 它内部持有一个
Real Subject对象的引用。 - 它负责控制对
Real Subject的访问。当客户端调用代理的方法时,代理可以执行一些预处理(如权限检查),然后决定是否以及如何调用Real Subject的同名方法,最后还可以进行一些后处理(如记日志)。
- 也实现了
几种常见的代理类型:
- **保护代理 (Protection Proxy)**:用于控制访问权限。
- **虚拟代理 (Virtual Proxy)**:用于延迟加载,当代理对象的方法被调用时,才真正创建和加载真实对象。
- **远程代理 (Remote Proxy)**:作为远程对象的本地代表,封装网络通信的细节。
- 日志代理 (Logging Proxy) / **缓存代理 (Caching Proxy)**:用于在方法调用前后添加日志或缓存逻辑。
Java 中的动态代理
除了手动编写代理类(静态代理),Java 还提供了一种更强大的机制——**动态代理 (Dynamic Proxy)**。
- 静态代理:需要为每一个真实主体类手动编写一个代理类。如果接口增多,代理类也会增多。
- 动态代理:不需要手动创建代理类文件。在运行时,通过
java.lang.reflect.Proxy类和InvocationHandler接口,可以为一个或多个接口动态地生成代理对象。你只需要编写一个InvocationHandler(调用处理器),在这个处理器里定义通用的代理逻辑(如日志、权限检查),这个逻辑就可以应用到所有被代理的接口方法上。 - 应用场景:Spring AOP(面向切面编程)、RPC 框架(如 Dubbo)、MyBatis 的 Mapper 接口等,都大量使用了动态代理技术。
四、怎么做 (How - The Blueprint)
基本步骤(以静态的图片查看器虚拟代理为例):
- **定义主体接口
Image**,包含一个display()方法。 - **创建真实主体类
HighResolutionImage**,实现Image接口。它的构造函数会模拟一个耗时的加载过程。 - **创建代理类
ImageProxy**,也实现Image接口。- 它内部持有一个
HighResolutionImage的引用,但初始时为null。 - 在
display()方法中,检查引用是否为null。如果是,则new一个HighResolutionImage实例(此时才真正加载图片),然后调用其display()方法。如果不是null,则直接调用。
- 它内部持有一个
- 客户端创建和使用的是
ImageProxy对象。
Java 示例代码 (静态虚拟代理)
1 | // 1. 主体接口 (Subject) |
五、UML 类图
1 | @startuml |
图示解读:
ImageProxy和HighResolutionImage都实现了Image接口,因此对客户端是透明的。ImageProxy内部持有一个对HighResolutionImage的引用,实现了对真实对象的控制。- 客户端
ProxyPatternDemo只依赖于Image接口。
六、优缺点分析
优点 (Pros):
- 解耦:代理模式将客户端与真实主体解耦,客户端无需知道真实主体的存在。
- 职责清晰:真实主体只关心核心业务逻辑,而代理则负责处理额外的控制逻辑,符合单一职责原则。
- 高扩展性:可以在不修改真实主体的情况下,通过增加新的代理类来添加新功能。
- 强大的控制能力:代理模式提供了对真实对象访问的精细化控制,可以实现权限、缓存、懒加载等多种功能。
缺点 (Cons):
- 增加系统复杂性:引入了代理类,增加了系统中类的数量,可能会增加理解成本。
- 可能导致请求处理变慢:由于在客户端和真实主体之间增加了一层,某些情况下可能会轻微影响请求的处理速度(尽管对于懒加载、缓存等场景,总体性能是提升的)。
七、在 Java 中的应用
代理模式是 Java 世界的基石之一。
- RMI (Remote Method Invocation): 这是远程代理的经典应用。当你获取一个远程对象的引用时,你得到的其实是一个本地的代理对象(称为 Stub)。你调用这个 Stub 的方法,它会负责打包参数、通过网络发送给远程服务器、等待结果、解包并返回给你,对你来说就像调用本地对象一样。
- Spring AOP: Spring 的面向切面编程(AOP)功能,如事务管理(
@Transactional)、安全控制、日志记录等,就是通过动态代理实现的。Spring 在运行时为你需要被增强的 Bean(真实主体)创建一个代理对象,在这个代理对象中织入了切面逻辑(如开启事务、提交/回滚事务)。 - Hibernate/JPA: 这些 ORM 框架中的实体对象懒加载(Lazy Loading)功能,是虚拟代理的完美体现。当你从数据库加载一个
Order对象时,它关联的Customer对象可能并不会被立即加载,此时order.getCustomer()返回的是一个Customer的代理对象。只有当你第一次调用这个代理对象的任何方法(如customer.getName())时,Hibernate 才会发出 SQL 去数据库真正加载Customer的数据。