文章目录
-
- 前言
- 一、核心定义
- 二、标准体系结构图
- 三、场景推演
-
- [3.1 静态代理](#3.1 静态代理)
- [3.2 动态代理](#3.2 动态代理)
- 四、实战案例
-
- [4.1 需求分析](#4.1 需求分析)
- [4.2 架构图](#4.2 架构图)
- [4.3 类图](#4.3 类图)
- [4.4 时序图](#4.4 时序图)
- [4.5 代码分析](#4.5 代码分析)
-
- [4.5.1 自定义注解 `@Select`(模拟 MyBatis 的 SQL 注解)](#4.5.1 自定义注解
@Select(模拟 MyBatis 的 SQL 注解)) - [4.5.2 DAO 接口 `IUserDao`(只定义接口,无实现类)](#4.5.2 DAO 接口
IUserDao(只定义接口,无实现类)) - [4.5.3 代理工厂 `MapperFactoryBean`(核心:JDK 动态代理)](#4.5.3 代理工厂
MapperFactoryBean(核心:JDK 动态代理)) - [4.5.4 Bean 注册器 `RegisterBeanFactory`(核心:Spring 扩展点)](#4.5.4 Bean 注册器
RegisterBeanFactory(核心:Spring 扩展点)) - [4.5.5 Spring 配置文件 `spring-config.xml`](#4.5.5 Spring 配置文件
spring-config.xml) - [4.5.6 测试验证](#4.5.6 测试验证)
- [4.5.1 自定义注解 `@Select`(模拟 MyBatis 的 SQL 注解)](#4.5.1 自定义注解
- 总结
前言
在技术成长的路上,业务开发中的 CRUD 虽然熟悉,但框架和中间件背后的机制却常常被忽视。
你有没有想过:MyBatis 的 Mapper 接口只有接口定义,没有实现类,调用时却能执行 SQL 并返回数据------这是怎么做到的?
答案就是代理模式(Proxy Pattern) 。MyBatis-Spring 在启动时扫描 DAO 接口,为每一个接口动态生成一个代理类,将该代理类注册到 Spring 容器中 。调用接口方法时,实际上是代理类拦截了调用,从方法上的 @Select 等注解中读取 SQL,交给 SqlSession 执行,再将结果返回。
本章通过手写一个迷你版 MyBatis-Spring 代理注册流程,来深入理解代理模式的核心思想。
一、核心定义
代理模式(Proxy Pattern) 是一种结构型设计模式,为另一个对象提供一个代理(替身或占位符),以控制对原对象的访问。
- 核心思想:在客户端与真实服务对象之间插入一个代理层,由代理负责访问控制、日志、缓存、懒加载等附加功能,而不改变真实对象
- 解决问题:直接访问真实对象有困难(远程、权限控制、重量级实例创建)或需要在访问前后增加逻辑
- 代理的三种常见分类:
| 类型 | 说明 | 本案例 |
|---|---|---|
| 静态代理 | 编译期生成代理类,代码量大,扩展性差 | ✗ |
| JDK 动态代理 | 运行期通过 Proxy.newProxyInstance() + InvocationHandler 生成 |
✅ 本案例使用 |
| CGLIB 代理 | 运行期通过字节码增强生成子类,可代理无接口的类 | ✗ |
二、标准体系结构图
实现
实现
持有引用(委托)
通过接口访问(不感知代理)
<<interface>>
Subject
+request() : void
RealSubject
+request() : void
Proxy
-realSubject: RealSubject
+Proxy(realSubject: RealSubject)
+request() : void
-checkAccess() : void
-logAccess() : void
Client
| 角色 | 本案例对应 | 说明 |
|---|---|---|
| Subject(抽象主题) | IUserDao 接口 |
定义真实对象和代理对象的公共接口 |
| RealSubject(真实主题) | 无(本例没有真实实现类) | 在 MyBatis 场景中,真正没有手写实现类 |
| Proxy(代理) | MapperFactoryBean 内的 InvocationHandler |
运行期动态生成,拦截接口方法调用,执行 SQL |
| ProxyFactory(代理工厂) | MapperFactoryBean + RegisterBeanFactory |
负责创建代理对象并将其注册到 Spring 容器 |
| Client(客户端) | ApiTest |
从 Spring 容器取出 IUserDao 直接调用,不感知代理存在 |
三、场景推演
3.1 静态代理
场景:支付系统日志统计
需求:
- 有一个支付服务
PaymentService,提供pay()方法。 - 我们希望在支付操作前后:
- 打印日志 → 记录支付开始和结束时间
- 不想修改原支付类的代码
特点:
- 静态代理需要手动写代理类。
- 每个真实对象都需要一个对应的代理类。
优点:
- 简单直观,逻辑清晰
- 可以控制访问、增加额外逻辑
缺点:
- 每个真实对象都要写一个代理类 → 代码重复
- 扩展性差,维护成本高
使用场景:
- 对象固定、代理逻辑简单、不会频繁改变
代码实现:
java
// 1. 抽象接口
public interface PaymentService {
void pay(double amount);
}
// 2. 真实对象
public class RealPaymentService implements PaymentService {
@Override
public void pay(double amount) {
System.out.println("Processing payment of $" + amount);
}
}
// 3. 静态代理对象
public class PaymentServiceProxy implements PaymentService {
private PaymentService realService;
public PaymentServiceProxy(PaymentService realService) {
this.realService = realService;
}
@Override
public void pay(double amount) {
// 代理增强逻辑:日志
System.out.println("Log: Payment start");
realService.pay(amount);
System.out.println("Log: Payment end");
}
}
// 4. 客户端调用
public class Client {
public static void main(String[] args) {
PaymentService real = new RealPaymentService();
PaymentService proxy = new PaymentServiceProxy(real);
proxy.pay(100);
}
}
3.2 动态代理
场景:接口方法执行耗时统计
需求:
- 有一个接口
Calculator提供add和subtract方法。 - 希望在方法调用前后:
- 统计执行耗时
- 不想写重复的代理类 → 所有接口实现对象都可动态代理
特点:
- 动态代理基于接口(JDK Proxy)或类(Cglib)
- 在运行时生成代理对象,不需要手写
优点:
- 无需手写代理类,统一代理处理
- 易于扩展 → 任何实现接口的对象都可代理
- 可插入日志、权限、缓存、性能统计等
缺点:
- 只能代理接口(JDK Proxy)
- 性能略低于静态代理(反射调用)
使用场景:
- AOP(切面编程) → Spring 就是基于动态代理
- 日志、权限、缓存、事务控制
- 对象数量多或变化频繁时特别适合
代码实现:
java
import java.lang.reflect.*;
// 1. 接口
public interface Calculator {
int add(int a, int b);
int subtract(int a, int b);
}
// 2. 真实对象
public class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) { return a + b; }
@Override
public int subtract(int a, int b) { return a - b; }
}
// 3. 动态代理调用处理器
public class TimeInvocationHandler implements InvocationHandler {
private Object target;
public TimeInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("Method " + method.getName() + " start");
Object result = method.invoke(target, args);
long end = System.currentTimeMillis();
System.out.println("Method " + method.getName() + " end, elapsed: " + (end - start) + "ms");
return result;
}
}
// 4. 客户端调用
public class Client {
public static void main(String[] args) {
Calculator calc = new CalculatorImpl();
Calculator proxy = (Calculator) Proxy.newProxyInstance(
calc.getClass().getClassLoader(),
calc.getClass().getInterfaces(),
new TimeInvocationHandler(calc)
);
proxy.add(3, 5);
proxy.subtract(10, 4);
}
}
四、实战案例
4.1 需求分析
问题:MyBatis 的 Mapper 接口如何无需实现类就能执行 SQL?
普通接口调用链(需要实现类):
Client → IUserDao(接口)→ UserDaoImpl(实现类,手写 JDBC)→ DB
MyBatis 代理调用链(无需实现类):
Client → IUserDao(接口)
↓(Spring 容器注入的其实是代理对象)
$Proxy0(JDK动态生成)→ InvocationHandler.invoke()
↓
读取 @Select("select userName from user where id = #{uId}")
↓(MyBatis 中真实流程)
SqlSession.selectOne(sql, args) → DB → 返回结果
本案例模拟的核心步骤:
- 定义接口
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 类图
方法上标注
mapperInterface 持有接口类
getObject() 内创建
从 method 读取注解
定义并注册到 Spring
beanFactory.getBean() 取代理对象调用
<<注解 @interface>>
Select
+value() : String
<<interface>>
IUserDao
+queryUserInfo(uId: String) : String
<<代理工厂 FactoryBean>>
MapperFactoryBean<T>
-logger: Logger
-mapperInterface: Class<T>
+MapperFactoryBean(mapperInterface: Class<T>)
+getObject() : T
+getObjectType() : Class<?>
+isSingleton() : boolean
<<Bean注册器 BeanDefinitionRegistryPostProcessor>>
RegisterBeanFactory
+postProcessBeanDefinitionRegistry(registry: BeanDefinitionRegistry) : void
+postProcessBeanFactory(factory: ConfigurableListableBeanFactory) : void
<<JDK代理核心>>
InvocationHandler
+invoke(proxy, method, args) : Object
ApiTest
+test_IUserDao() : void
FactoryBean 的 getObject() 返回的是\nProxy.newProxyInstance() 生成的动态代理对象\n而不是 MapperFactoryBean 本身
Spring 启动时调用\npostProcessBeanDefinitionRegistry()\n将代理 bean 以 'userDao' 名注册到容器
4.4 时序图
ApiTest Proxy0(JDK动态代理) MapperFactoryBean RegisterBeanFactory Spring容器启动 ApiTest Proxy0(JDK动态代理) MapperFactoryBean RegisterBeanFactory Spring容器启动 Spring 启动,加载 spring-config.xml 创建 GenericBeanDefinition setBeanClass(MapperFactoryBean.class) addConstructorArg(IUserDao.class) "userDao" bean 定义注册完成 Spring 初始化 bean 创建 InvocationHandler 调用 Proxy.newProxyInstance() "userDao" bean = Proxy0,容器初始化完成 InvocationHandler.invoke() 被触发 Select select = method.getAnnotation(Select.class) select.value() = "select userName from user where id = logger.info("SQL: {}", "select userName from user where id = 100001") postProcessBeanDefinitionRegistry(registry) registerBeanDefinition("userDao", beanDef) new MapperFactoryBean(IUserDao.class) getObject()(FactoryBean 接口) 返回 Proxy0(IUserDao 的代理实例) beanFactory.getBean("userDao", IUserDao.class) 返回 $Proxy0(客户端看到的是 IUserDao 接口) userDao.queryUserInfo("100001") "100001 小傅哥,沉淀、分享、成长..."
4.5 代码分析
4.5.1 自定义注解 @Select(模拟 MyBatis 的 SQL 注解)
java
// agent/Select.java
@Documented
@Retention(RetentionPolicy.RUNTIME) // 运行期保留,否则代理中无法通过反射读取
@Target({ElementType.METHOD}) // 作用于方法级别
public @interface Select {
String value() default ""; // 存储 SQL 语句
}
4.5.2 DAO 接口 IUserDao(只定义接口,无实现类)
java
// IUserDao.java
public interface IUserDao {
// 用注解指定 SQL,MyBatis 风格:#{uId} 是占位符
@Select("select userName from user where id = #{uId}")
String queryUserInfo(String uId);
}
关键点:这里只有接口,没有任何实现类。调用时能工作,完全依赖代理机制。
4.5.3 代理工厂 MapperFactoryBean(核心:JDK 动态代理)
java
// agent/MapperFactoryBean.java
public class MapperFactoryBean<T> implements FactoryBean<T> {
private Class<T> mapperInterface; // 被代理的接口 Class 对象
public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface; // 通过构造函数接收接口类型
}
@Override
public T getObject() throws Exception {
// ① InvocationHandler:所有接口方法调用都会路由到这里
// proxy:代理对象本身
// method:当前被调用的方法,比如 queryUserInfo
// args:方法参数数组,比如 ["100001"]
InvocationHandler handler = (proxy, method, args) -> {
// ② 从方法上读取 @Select 注解,获取 SQL 语句
Select select = method.getAnnotation(Select.class);
// ③ 模拟 SQL 执行(真实 MyBatis 中会调用 SqlSession 执行 SQL)
logger.info("SQL:{}", select.value().replace("#{uId}", args[0].toString()));
// ④ 模拟返回结果
return args[0] + " 代理模式测试";
};
// ⑤ JDK 动态代理:生成实现了 mapperInterface 的代理对象
return (T) Proxy.newProxyInstance(
this.getClass().getClassLoader(),
new Class[]{mapperInterface},
handler
);
}
@Override
public Class<?> getObjectType() {
return mapperInterface; // 告诉 Spring 这个 FactoryBean 产出的 bean 类型
}
@Override
public boolean isSingleton() {
return false; // 代理对象非单例(每次 getObject() 生成新代理)
}
}
FactoryBean<T> 的特殊性:
当 Spring 容器中有一个 bean 实现了
FactoryBean<T>接口时,beanFactory.getBean("userDao")返回的不是MapperFactoryBean的实例,而是getObject()的返回值------即代理对象。这是 Spring 中实现代理注入的关键机制,MyBatis-Spring 的MapperFactoryBean正是同名同原理。
4.5.4 Bean 注册器 RegisterBeanFactory(核心:Spring 扩展点)
java
// agent/RegisterBeanFactory.java
public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
// ① 创建 bean 定义:指定产生此 bean 的工厂类为 MapperFactoryBean
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(MapperFactoryBean.class);
beanDefinition.setScope("singleton");
// ② 为 MapperFactoryBean 的构造函数传入 IUserDao.class
// 相当于:new MapperFactoryBean(IUserDao.class)
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(IUserDao.class);
// ③ 创建 BeanDefinitionHolder,指定 bean 在容器中的名称为 "userDao"
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao");
// ④ 注册到 DefaultListableBeanFactory(Spring 默认 bean 注册中心)
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
// 此处留空,本案例不需要在此扩展点做额外处理
}
}
三个关键 Spring API:
| API | 职责 |
|---|---|
BeanDefinitionRegistryPostProcessor |
Spring 扩展点接口,在所有 bean 定义加载后、bean 初始化前回调,可在此动态注册新的 bean 定义 |
GenericBeanDefinition |
描述一个 bean 的元信息(用哪个类、什么作用域、构造参数等) |
BeanDefinitionReaderUtils.registerBeanDefinition |
将 bean 定义写入 BeanDefinitionRegistry(即 DefaultListableBeanFactory) |
4.5.5 Spring 配置文件 spring-config.xml
xml
<!-- spring-config.xml -->
<beans ...>
<!-- 只需要配置 RegisterBeanFactory,它会在启动时自动注册代理 bean -->
<bean id="userDao" class="com.likerhood.design.agent.RegisterBeanFactory"/>
</beans>
注意 :这里配置的 bean class 是 RegisterBeanFactory(注册器),而不是 IUserDao 或 MapperFactoryBean。RegisterBeanFactory 实现了 BeanDefinitionRegistryPostProcessor,Spring 会在容器启动阶段特殊处理它,调用其 postProcessBeanDefinitionRegistry() 方法动态注册 userDao 代理 bean。
4.5.6 测试验证
java
// ApiTest.java
@Test
public void test_IUserDao() {
// ① 加载 spring-config.xml,触发 Spring 容器初始化
// → RegisterBeanFactory.postProcessBeanDefinitionRegistry() 被调用
// → MapperFactoryBean.getObject() 被调用
// → "userDao" = $Proxy0(IUserDao 的代理对象)注册完成
BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
// ② 从容器取 "userDao"(取到的是代理对象,不是 MapperFactoryBean)
IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
// ③ 调用接口方法(实际进入 InvocationHandler.invoke())
String res = userDao.queryUserInfo("100001");
logger.info("测试结果:{}", res);
}
// 测试输出:
// INFO MapperFactoryBean - SQL:select userName from user where id = 100001
// INFO ApiTest - 测试结果:100001 小傅哥,沉淀、分享、成长,让自己和他人都能有所收获!
总结
| 维度 | 传统 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 外观模式:外观对外提供简化接口(多个子系统 → 一个接口),代理是对单一对象的封装(一个接口 → 同一接口,透明)