本文是【GoF设计模式】系列第7篇,更多内容欢迎关注公众号:咖啡八杯
前言
为什么需要代理模式?
有时候我们不能或不想直接访问某个对象。比如对象创建开销很大需要延迟加载,或者需要在访问前做权限检查,或者需要记录访问日志。直接在业务代码中掺杂这些逻辑会让代码臃肿且难以维护。
代理模式通过引入一个中间层,将这些控制逻辑从业务代码中分离出来。客户端代码不需要知道它是在和代理交互还是真实对象交互,两者可以透明替换。
概念
代理模式Proxy Pattern是一种结构型设计模式 ,核心思想是为一个对象提供一个替身(代理),以控制对这个对象的访问。
代理对象和真实对象实现相同的接口,客户端通过代理间接访问真实对象,代理可以在调用前后添加额外的控制逻辑。
代理模式的主要角色有:
- Subject(抽象主题):声明真实主题和代理共同实现的业务方法,客户端面向该接口编程
- RealSubject(真实主题):定义代理所代表的真实对象,是客户端最终要访问的对象
- Proxy(代理) :持有对真实主题的引用,实现与真实主题相同的接口,在调用真实主题前后添加控制逻辑
类图展示了静态结构,但对代理模式这种"调用拦截"的场景,时序图更能体现动态调用流程:
sequenceDiagram participant Client participant Proxy participant RealSubject Client->>Proxy: request() Proxy->>Proxy: 前置处理(权限/日志/延迟加载) Proxy->>RealSubject: request() RealSubject->>Proxy: 返回结果 Proxy->>Proxy: 后置处理(日志/缓存) Proxy->>Client: 返回结果
可以把代理理解为私人助理:老板(客户端)有事找某人(真实对象),先通过助理(代理)。助理可以在前面挡掉不重要的打扰(权限控制),也可以在事后记录行程(日志),老板全程不需要直接接触对方。这个比喻贯穿后面的实现章节,方便对照理解。
实现
代理模式的基本实现分为以下几个步骤:
- 定义抽象主题,一般是接口或抽象类,声明真实主题和代理对象实现的业务方法
- 定义真实主题,实现抽象主题中的具体业务
- 定义代理类,包含对
RealSubject的引用,提供和真实主题相同的接口,在调用前后添加控制逻辑 - 客户端使用代理
java
// 抽象主题
interface Subject {
// 声明同业务对象同名的方法
public void request();
}
// 真实主题
class RealSubject implements Subject {
public void request() {
System.out.println("RealSubject request");
}
}
// 代理类
class Proxy implements Subject {
private RealSubject realSubject;
@Override
public void request() {
// 访问真实主题之前:延迟加载
if (realSubject == null) {
realSubject = new RealSubject();
}
// 调用真实主题的方法
realSubject.request();
// 访问真实主题之后:可添加日志等逻辑
}
}
总结
代理模式本质上是一层"中间人"------为真实对象提供一个替身,在调用前后添加控制逻辑。
什么时候用:
- 想控制对某个对象的访问(权限、延迟加载、缓存、日志)
- 需要为远程对象提供本地代表
- 引入第三方库或遗留代码,需要统一调用方式
什么时候不用:
- 接口差异巨大,代理会变得臃肿
- 能修改真实对象源码且代价不大,直接修改更简单
- 系统设计阶段就能定义接口规范,从源头统一即可
简单记忆:
代理解决"控制访问"的问题,是给真实对象"加一层控制"。能改源码就改,改不了才用代理。
代理 vs 装饰器 vs 适配器 vs 外观:四个结构型模式都"包了一层对象",结构相似但意图不同:
| 模式 | 接口关系 | 核心意图 |
|---|---|---|
| 代理 | 目标接口 = 被包装对象接口 | 控制访问,附加访问前后逻辑 |
| 装饰器 | 目标接口 = 被包装对象接口 | 增强功能,接口不变 |
| 适配器 | 目标接口 ≠ 被包装对象接口 | 转换接口,让不兼容的类协同 |
| 外观 | 目标接口是新设计的 | 简化复杂子系统的调用 |
口诀对比:代理控访问,装饰增功能,适配改接口,外观简调用。
代理模式 vs 中介者模式
两者都引入"中间层",结构相似,但意图完全不同:
| 维度 | 代理模式 | 中介者模式 |
|---|---|---|
| 核心意图 | 控制对单个对象的访问 | 协调多个对象之间的交互 |
| 对象关系 | 客户端 → 代理 → 真实对象(单向委托) | 多个同事对象 ↔ 中介者 ↔ 多个同事对象(多向协调) |
| 封装内容 | 访问控制逻辑(权限、延迟加载、缓存) | 对象间的交互规则、通信协议 |
| 客户端感知 | 客户端不知道真实对象存在 | 各同事对象知道中介者存在,但不直接知道其他同事 |
| 应用场景 | Spring AOP、MyBatis Mapper、远程调用 | MVC 框架的 Controller、聊天室服务器、GUI 事件分发 |
用例子说明:
- 代理模式:你要见 CEO,先通过秘书(代理)。秘书控制访问------过滤不重要的人、安排时间。你只和秘书打交道,CEO 对你透明。
- 中介者模式:公司的各部门(销售、研发、财务)不直接相互沟通,所有协调通过行政部(中介者)。销售要研发资源,找行政部安排;财务要销售数据,找行政部转发。各部门知道行政部,但不直接依赖其他部门。
简单记忆:
代理管"谁能动",中介者管"怎么联动"。代理是单对象的门禁,中介者是多对象的调度中心。
常见误区:
- 误区:代理模式 = 装饰器模式 → 意图不同:一个控访问,一个增功能
- 误区:代理必须和真实对象同接口 → 对,这是代理的基本要求,否则就不是代理了
- 误区:Spring
@Transactional在同一个类内部调用不生效 → 这是自调用绕过代理的经典坑,AOP 代理需要外部调用才能触发拦截逻辑
练习题目
门禁系统权限控制
题目描述:某科技园区的门禁系统管理着多个房间,每个房间有名称和最低访问权限等级。用户通过门禁终端访问房间,门禁终端作为代理,会检查用户的权限等级:
- 用户权限 ≥ 房间要求的等级 → 放行,房间显示欢迎信息
- 用户权限 < 房间要求的等级 → 拒绝,房间不会响应
请使用代理模式实现该门禁系统。其中:
- Room(真实对象)拥有 enter() 方法,输出欢迎信息
- RoomProxy(代理)在调用 enter() 前进行权限检查,只有通过才委托给真实对象
输入描述:第一行输入一个整数,表示用户的权限等级。第二行输入一个整数 N(1 ≤ N ≤ 20),表示要访问的房间数量。接下来 N 行,每行包含房间名称和该房间要求的最低权限等级,用空格分隔。
输出描述:对每个房间,通过代理访问后输出:
- 权限足够:
欢迎进入[房间名] - 权限不足:
权限不足,无法进入[房间名]
输入示例:
2
3
MeetingRoom 1
Lab 3
Office 2
输出示例:
欢迎进入MeetingRoom
权限不足,无法进入Lab
欢迎进入Office
解题思路 :代理类 RoomProxy 实现 Room 接口,内部持有用户权限。在 enter() 方法中先检查用户权限是否满足房间要求,满足则委托给 RealRoom.enter(),否则直接拒绝。这体现了代理模式的核心------代理在调用真实对象之前增加控制逻辑(权限校验),对客户端透明。
java
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int qx = sc.nextInt();
int n = sc.nextInt();
RoomProxy proxy = new RoomProxy(qx);
while (n-- > 0) {
String name = sc.next();
int roomQx = sc.nextInt();
RealRoom room = new RealRoom(name, roomQx);
proxy.setRoom(room);
proxy.enter();
}
}
}
interface Room {
public void enter();
}
class RealRoom implements Room {
private String name;
private int qx;
public RealRoom(String name, int qx) {
this.name = name;
this.qx = qx;
}
public void enter() {
System.out.println("欢迎进入" + this.name);
}
public String getName() {
return this.name;
}
public int getQx() {
return this.qx;
}
}
class RoomProxy implements Room {
private int qx;
private RealRoom room;
public RoomProxy(int qx) {
this.qx = qx;
}
public void setRoom(RealRoom room) {
this.room = room;
}
public void enter() {
if (this.qx >= room.getQx()) {
room.enter();
} else {
System.out.println("权限不足,无法进入" + room.getName());
}
}
}
扩展:实际项目中的代理模式
Spring AOP 的动态代理
Spring AOP 是代理模式最经典的应用。当 Bean 被 AOP 增强时,Spring 不会返回原始对象,而是返回一个代理对象。代理在方法调用前后插入切面逻辑(事务、日志、权限校验等),对调用方完全透明。
java
// 业务代码:完全感知不到代理的存在
@Service
public class OrderService {
@Transactional // 事务由代理自动管理
public void createOrder(Order order) {
orderDao.save(order);
inventoryService.deduct(order.getProductId(), order.getQty());
}
}
// Spring 内部:创建代理对象(JDK 动态代理或 CGLIB)
// 代理在 createOrder 前后自动开启/提交/回滚事务
关键点 :Spring 默认对实现了接口的 Bean 使用 JDK 动态代理,对没有实现接口的 Bean 使用 CGLIB 代理。开发者只需写 @Transactional、@Cacheable 等注解,代理负责增强逻辑的织入。
JDK 动态代理 vs CGLIB 对比:
| 对比维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 原理 | 实现接口,运行时生成 Proxy 类 | 继承目标类,生成子类 |
| 要求 | 目标必须实现接口 | 目标类不能是 final |
| Spring 默认场景 | 有接口的 Bean | 无接口的 Bean |
| 性能(高版本 JDK) | 较高 | 较低 |
MyBatis 的 Mapper 代理
MyBatis 中我们只写 Mapper 接口,不需要写实现类,就能直接调用方法执行 SQL。这是因为 MyBatis 在启动时为每个 Mapper 接口生成了代理对象。
java
// 开发者只写接口
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User findById(Long id);
}
// 使用:直接注入接口,实际上是代理对象
@Service
public class UserService {
@Autowired
private UserMapper userMapper; // 这是代理对象
public User getUser(Long id) {
return userMapper.findById(id); // 代理拦截调用,执行 SQL
}
}
// MyBatis 内部:MapperProxy 拦截接口方法调用
// 根据方法上的注解或 XML 映射,执行对应的 SQL 语句
关键点 :MapperProxy 实现了 InvocationHandler 接口,拦截所有方法调用,将接口方法映射为 SQL 执行。这是 JDK 动态代理的典型应用,让接口方法调用变成了数据库操作。
Java 动态代理实现 AOP 拦截器
Java 原生提供了 java.lang.reflect.Proxy 和 InvocationHandler 接口,可以在运行时动态创建代理类。这是实现 AOP、拦截器等机制的基础。
java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 通用的日志拦截器
public class LogInterceptor implements InvocationHandler {
private Object target; // 被代理的真实对象
public LogInterceptor(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[LOG] 调用方法: " + method.getName());
long start = System.currentTimeMillis();
Object result = method.invoke(target, args); // 调用真实对象
long elapsed = System.currentTimeMillis() - start;
System.out.println("[LOG] 方法 " + method.getName() + " 耗时: " + elapsed + "ms");
return result;
}
// 工具方法:创建代理对象
@SuppressWarnings("unchecked")
public static <T> T createProxy(T target) {
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LogInterceptor(target));
}
}
// 使用
interface UserService {
public User findById(Long id);
}
class UserServiceImpl implements UserService {
public User findById(Long id) {
// 查询数据库...
return new User(id, "张三");
}
}
// 创建代理
UserService proxy = LogInterceptor.createProxy(new UserServiceImpl());
proxy.findById(1L);
// 输出:
// [LOG] 调用方法: findById
// [LOG] 方法 findById 耗时: 3ms
关键点 :Proxy.newProxyInstance 在运行时生成一个实现了指定接口的代理类,所有方法调用都会被转发到 InvocationHandler.invoke。这是 JDK 动态代理,要求目标类必须实现接口。如果目标类没有接口,需要使用 CGLIB(通过继承生成子类代理)。
Guava 的 Suppliers.memoize 懒加载代理
Guava 提供了 Suppliers.memoize,本质上是一个缓存代理:第一次调用时执行真实的 Supplier 获取值,后续调用直接返回缓存的结果。
java
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
// 模拟一个开销大的初始化操作
Supplier<DatabaseConnection> expensiveInit = new Supplier<DatabaseConnection>() {
@Override
public DatabaseConnection get() {
System.out.println("正在建立数据库连接...");
// 模拟耗时操作
try { Thread.sleep(3000); } catch (InterruptedException e) {}
return new DatabaseConnection("jdbc:mysql://localhost:3306/db");
}
};
// 用 memoize 包装:第一次调用执行初始化,后续直接返回缓存
Supplier<DatabaseConnection> cached = Suppliers.memoize(expensiveInit);
// 第一次调用:触发初始化,耗时 3 秒
DatabaseConnection conn1 = cached.get(); // 输出:正在建立数据库连接...
// 第二次调用:直接返回缓存,瞬间完成
DatabaseConnection conn2 = cached.get(); // 无输出,直接返回
关键点 :Suppliers.memoize 内部用一个代理 Supplier 持有真实 Supplier 和缓存值,首次调用后将结果缓存,后续调用直接返回缓存。这是虚拟代理 + 缓存代理的结合,synchronized 保证线程安全。
Nginx 的反向代理
Nginx 作为反向代理服务器,是代理模式在架构层面的典型应用。客户端请求到达 Nginx,Nginx 将请求转发给后端的真实服务器,然后将响应返回给客户端。客户端只知道 Nginx 的地址,不知道后端服务器的存在。
nginx
# nginx.conf
http {
upstream backend {
server 192.168.1.10:8080 weight=3; # 后端服务器1
server 192.168.1.11:8080 weight=1; # 后端服务器2
}
server {
listen 80;
location /api/ {
proxy_pass http://backend; # 代理转发
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
关键点:Nginx 代理在转发前后可以做很多事情------负载均衡(选择后端服务器)、权限校验(限制 IP)、缓存(静态资源直接返回)、日志记录(记录请求耗时)。这正是代理模式"控制访问"思想在架构层面的体现。