设计模式 · 代理模式(Proxy Pattern)java
设计模式 · 代理模式(Proxy Pattern)
前言
在技术成长的路上,业务开发中的 CRUD 虽然熟悉,但框架和中间件背后的机制却常常被忽视。
你有没有想过:MyBatis 的 Mapper 接口只有接口定义,没有实现类,调用时却能执行 SQL 并返回数据——这是怎么做到的?
答案就是代理模式(Proxy Pattern)。MyBatis-Spring 在启动时扫描 DAO 接口,为每一个接口动态生成一个代理类,将该代理类注册到 Spring 容器中。调用接口方法时,实际上是代理类拦截了调用,从方法上的 @Select 等注解中读取 SQL,交给 SqlSession 执行,再将结果返回。
本章通过手写一个迷你版 MyBatis-Spring 代理注册流程,来深入理解代理模式的核心思想。
一、核心定义
代理模式(Proxy Pattern) 是一种结构型设计模式,为另一个对象提供一个代理(替身或占位符),以控制对原对象的访问。
- 核心思想:在客户端与真实服务对象之间插入一个代理层,由代理负责访问控制、日志、缓存、懒加载等附加功能,而不改变真实对象
- 解决问题:直接访问真实对象有困难(远程、权限控制、重量级实例创建)或需要在访问前后增加逻辑
- 代理的三种常见分类:
| 类型 | 说明 | 本案例 |
|---|---|---|
| 静态代理 | 编译期生成代理类,代码量大,扩展性差 | ✗ |
| JDK 动态代理 | 运行期通过 Proxy.newProxyInstance() + InvocationHandler 生成 |
✅ 本案例使用 |
| CGLIB 代理 | 运行期通过字节码增强生成子类,可代理无接口的类 | ✗ |
二、标准体系结构图
classDiagram
class Subject {
<<interface>>
+request() void
}
class RealSubject {
+request() void
}
class Proxy {
-realSubject: RealSubject
+Proxy(realSubject: RealSubject)
+request() void
-checkAccess() void
-logAccess() void
}
class Client {
}
Subject <|.. RealSubject : 实现
Subject <|.. Proxy : 实现
Proxy o--> RealSubject : 持有引用(委托)
Client --> Subject : 通过接口访问(不感知代理)
| 角色 | 本案例对应 | 说明 |
|---|---|---|
| Subject(抽象主题) | IUserDao 接口 |
定义真实对象和代理对象的公共接口 |
| RealSubject(真实主题) | 无(本例没有真实实现类) | 在 MyBatis 场景中,真正没有手写实现类 |
| Proxy(代理) | MapperFactoryBean 内的 InvocationHandler |
运行期动态生成,拦截接口方法调用,执行 SQL |
| ProxyFactory(代理工厂) | MapperFactoryBean + RegisterBeanFactory |
负责创建代理对象并将其注册到 Spring 容器 |
| Client(客户端) | ApiTest |
从 Spring 容器取出 IUserDao 直接调用,不感知代理存在 |
三、场景推演
3.1 静态代理
场景:支付系统日志统计
需求:
- 有一个支付服务
PaymentService,提供pay()方法。 - 我们希望在支付操作前后:
- 打印日志 → 记录支付开始和结束时间
- 不想修改原支付类的代码
特点:
- 静态代理需要手动写代理类。
- 每个真实对象都需要一个对应的代理类。
优点:
- 简单直观,逻辑清晰
- 可以控制访问、增加额外逻辑
缺点:
- 每个真实对象都要写一个代理类 → 代码重复
- 扩展性差,维护成本高
使用场景:
- 对象固定、代理逻辑简单、不会频繁改变
代码实现:
1 | // 1. 抽象接口 |
3.2 动态代理
场景:接口方法执行耗时统计
需求:
- 有一个接口
Calculator提供add和subtract方法。 - 希望在方法调用前后:
- 统计执行耗时
- 不想写重复的代理类 → 所有接口实现对象都可动态代理
特点:
- 动态代理基于接口(JDK Proxy)或类(Cglib)
- 在运行时生成代理对象,不需要手写
优点:
- 无需手写代理类,统一代理处理
- 易于扩展 → 任何实现接口的对象都可代理
- 可插入日志、权限、缓存、性能统计等
缺点:
- 只能代理接口(JDK Proxy)
- 性能略低于静态代理(反射调用)
使用场景:
- AOP(切面编程) → Spring 就是基于动态代理
- 日志、权限、缓存、事务控制
- 对象数量多或变化频繁时特别适合
代码实现:
1 | import java.lang.reflect.*; |
四、实战案例
4.1 需求分析
问题:MyBatis 的 Mapper 接口如何无需实现类就能执行 SQL?
1 | 普通接口调用链(需要实现类): |
本案例模拟的核心步骤:
- 定义接口
IUserDao,方法上加@Select(sql) MapperFactoryBean实现FactoryBean<T>:
getObject()中用Proxy.newProxyInstance()生成代理
InvocationHandler读取@Select注解的SQL,模拟执行RegisterBeanFactory实现BeanDefinitionRegistryPostProcessor:
在Spring启动时,将MapperFactoryBean包装的bean
以 “userDao“ 为名注册到Spring容器- 调用方通过
beanFactory.getBean("userDao", IUserDao.class)
拿到的是代理对象,调用queryUserInfo()触发invoke()
原因:代理模式在此处是一种框架/中间件级别的能力,其对比的不是”坏代码 vs 好代码”,而是展示了一种”没有代理机制时必须手写大量重复 DAO 实现类“与”有代理机制后只需定义接口“的本质差异。

- 图片来源:[重学 Java 设计模式:实战代理模式「模拟mybatis-spring中定义DAO接口,使用代理类方式操作数据库原理实现场景」 | 小傅哥 bugstack 虫洞栈](https://bugstack.cn/md/develop/design-pattern/2020-06-16-重学 Java 设计模式《实战代理模式》.html)
本案例实现的等效功能:
| 对比维度 | 传统 JDBC 方式 | 本案例代理方式 |
|---|---|---|
| 需要手写实现类? | 是,每个 DAO 都要写 XxxDaoImpl |
否,只定义接口 |
| SQL 放在哪里? | 硬编码在实现类中 | @Select 注解在方法上 |
| Spring 注入方式 | 直接注入实现类 bean | 代理工厂动态生成代理对象注入 |
| 扩展新 DAO 接口 | 写新实现类 + 配置 bean | 只需定义接口 + 加注解 |
4.2 架构图

4.3 类图
classDiagram
class Select {
<<注解 @interface>>
+value() String
}
class IUserDao {
<<interface>>
+queryUserInfo(uId: String) String
}
class MapperFactoryBean~T~ {
<<代理工厂 FactoryBean>>
-logger: Logger
-mapperInterface: Class~T~
+MapperFactoryBean(mapperInterface: Class~T~)
+getObject() T
+getObjectType() Class~?~
+isSingleton() boolean
}
class RegisterBeanFactory {
<<Bean注册器 BeanDefinitionRegistryPostProcessor>>
+postProcessBeanDefinitionRegistry(registry: BeanDefinitionRegistry) void
+postProcessBeanFactory(factory: ConfigurableListableBeanFactory) void
}
class InvocationHandler {
<<JDK代理核心>>
+invoke(proxy, method, args) Object
}
class ApiTest {
+test_IUserDao() void
}
IUserDao ..> Select : 方法上标注
MapperFactoryBean ..> IUserDao : mapperInterface 持有接口类
MapperFactoryBean ..> InvocationHandler : getObject() 内创建
InvocationHandler ..> Select : 从 method 读取注解
RegisterBeanFactory ..> MapperFactoryBean : 定义并注册到 Spring
ApiTest ..> IUserDao : beanFactory.getBean() 取代理对象调用
note for MapperFactoryBean "FactoryBean 的 getObject() 返回的是\nProxy.newProxyInstance() 生成的动态代理对象\n而不是 MapperFactoryBean 本身"
note for RegisterBeanFactory "Spring 启动时调用\npostProcessBeanDefinitionRegistry()\n将代理 bean 以 'userDao' 名注册到容器"
4.4 时序图
sequenceDiagram
participant Spring as Spring容器启动
participant RBF as RegisterBeanFactory
participant MFB as MapperFactoryBean
participant Proxy as $Proxy0(JDK动态代理)
participant Client as ApiTest
Note over Spring: Spring 启动,加载 spring-config.xml
Spring ->> RBF: postProcessBeanDefinitionRegistry(registry)
Note over RBF: 创建 GenericBeanDefinition<br/>setBeanClass(MapperFactoryBean.class)<br/>addConstructorArg(IUserDao.class)
RBF ->> Spring: registerBeanDefinition("userDao", beanDef)
Note over Spring: "userDao" bean 定义注册完成
Note over Spring: Spring 初始化 bean
Spring ->> MFB: new MapperFactoryBean(IUserDao.class)
Spring ->> MFB: getObject()(FactoryBean 接口)
Note over MFB: 创建 InvocationHandler<br/>调用 Proxy.newProxyInstance()
MFB -->> Spring: 返回 $Proxy0(IUserDao 的代理实例)
Note over Spring: "userDao" bean = $Proxy0,容器初始化完成
Client ->> Spring: beanFactory.getBean("userDao", IUserDao.class)
Spring -->> Client: 返回 $Proxy0(客户端看到的是 IUserDao 接口)
Client ->> Proxy: userDao.queryUserInfo("100001")
Note over Proxy: InvocationHandler.invoke() 被触发
Note over Proxy: Select select = method.getAnnotation(Select.class)
Note over Proxy: select.value() = "select userName from user where id = #{uId}"
Note over Proxy: logger.info("SQL: {}", "select userName from user where id = 100001")
Proxy -->> Client: "100001 小傅哥,沉淀、分享、成长..."
4.5 代码分析
4.5.1 自定义注解 @Select(模拟 MyBatis 的 SQL 注解)
1 | // agent/Select.java |
4.5.2 DAO 接口 IUserDao(只定义接口,无实现类)
1 | // IUserDao.java |
关键点:这里只有接口,没有任何实现类。调用时能工作,完全依赖代理机制。
4.5.3 代理工厂 MapperFactoryBean(核心:JDK 动态代理)
1 | // agent/MapperFactoryBean.java |
FactoryBean<T> 的特殊性:
当 Spring 容器中有一个 bean 实现了
FactoryBean<T>接口时,beanFactory.getBean("userDao")返回的不是MapperFactoryBean的实例,而是getObject()的返回值——即代理对象。这是 Spring 中实现代理注入的关键机制,MyBatis-Spring 的MapperFactoryBean正是同名同原理。
4.5.4 Bean 注册器 RegisterBeanFactory(核心:Spring 扩展点)
1 | // agent/RegisterBeanFactory.java |
三个关键 Spring API:
| API | 职责 |
|---|---|
BeanDefinitionRegistryPostProcessor |
Spring 扩展点接口,在所有 bean 定义加载后、bean 初始化前回调,可在此动态注册新的 bean 定义 |
GenericBeanDefinition |
描述一个 bean 的元信息(用哪个类、什么作用域、构造参数等) |
BeanDefinitionReaderUtils.registerBeanDefinition |
将 bean 定义写入 BeanDefinitionRegistry(即 DefaultListableBeanFactory) |
4.5.5 Spring 配置文件 spring-config.xml
1 | <!-- spring-config.xml --> |
注意:这里配置的 bean class 是 RegisterBeanFactory(注册器),而不是 IUserDao 或 MapperFactoryBean。RegisterBeanFactory 实现了 BeanDefinitionRegistryPostProcessor,Spring 会在容器启动阶段特殊处理它,调用其 postProcessBeanDefinitionRegistry() 方法动态注册 userDao 代理 bean。
4.5.6 测试验证
1 | // ApiTest.java |
总结
| 维度 | 传统 DAO 实现 | 代理模式(本案例) |
|---|---|---|
| 实现类 | 每个接口必须手写 XxxDaoImpl |
无需实现类,代理自动生成 |
| SQL 管理 | 硬编码在实现类中 | 通过 @Select 注解集中管理 |
| Spring 集成 | 直接注册实现类 bean | FactoryBean + BeanDefinitionRegistryPostProcessor 动态注册 |
| 扩展新接口 | 写新实现类、配置新 bean | 定义接口、加注解,框架自动处理 |
| 代码体积 | 随接口数量线性增长 | 框架代码固定,接口数量增加不增加框架代码 |
代理模式的关键实现要点:
- JDK 动态代理三要素:ClassLoader(指定代理类的类加载器)、Class[](代理要实现的接口列表)、InvocationHandler(所有方法调用的拦截器)——缺一不可。
FactoryBean的间接性:Spring 中FactoryBean.getObject()返回的才是真正注入的对象,这是代理模式与 Spring 结合的核心”技巧”,MyBatis-Spring、Dubbo 的 RPC 代理等都用了这个机制。BeanDefinitionRegistryPostProcessor扩展点:这是框架动态注册 bean 的正规入口,比 XML 配置更灵活,批量注册时尤为强大(MyBatis-Spring 的@MapperScan底层正是扫描包内所有接口,循环注册每个MapperFactoryBean)。
代理模式的适用场景:
| 场景 | 说明 |
|---|---|
| MyBatis Mapper | 接口代理,拦截方法调用执行 SQL |
| RPC 框架(Dubbo/gRPC) | 接口代理,拦截调用发起远程网络请求 |
| Spring AOP | CGLIB/JDK 代理,切面拦截方法调用插入逻辑 |
| 权限控制代理 | 在调用前检查权限,通过再转发给真实对象 |
| 缓存代理 | 在调用前查缓存,未命中再转发并写缓存 |
| 延迟加载代理 | 对象使用时才真正初始化(Hibernate 懒加载) |
与其他模式对比:
- 代理模式 vs 装饰器模式:代理控制对目标对象的访问(可以拒绝、缓存、远程化),装饰器为目标对象添加功能。代理通常自己管理目标对象的生命周期,装饰器中目标对象由调用方传入
- 代理模式 vs 适配器模式:适配器改变接口,代理不改变接口(接口完全相同,只是在调用链上插入一层)
- 代理模式 vs 外观模式:外观对外提供简化接口(多个子系统 → 一个接口),代理是对单一对象的封装(一个接口 → 同一接口,透明)