设计模式 · 代理模式(Proxy Pattern)

前言

在技术成长的路上,业务开发中的 CRUD 虽然熟悉,但框架和中间件背后的机制却常常被忽视。

你有没有想过:MyBatis 的 Mapper 接口只有接口定义,没有实现类,调用时却能执行 SQL 并返回数据——这是怎么做到的?

答案就是代理模式(Proxy Pattern)MyBatis-Spring 在启动时扫描 DAO 接口,为每一个接口动态生成一个代理类,将该代理类注册到 Spring 容器中。调用接口方法时,实际上是代理类拦截了调用,从方法上的 @Select 等注解中读取 SQL,交给 SqlSession 执行,再将结果返回。

本章通过手写一个迷你版 MyBatis-Spring 代理注册流程,来深入理解代理模式的核心思想。


一、核心定义

代理模式(Proxy Pattern) 是一种结构型设计模式,为另一个对象提供一个代理(替身或占位符),以控制对原对象的访问。

  • 核心思想:在客户端与真实服务对象之间插入一个代理层,由代理负责访问控制、日志、缓存、懒加载等附加功能,而不改变真实对象
  • 解决问题:直接访问真实对象有困难(远程、权限控制、重量级实例创建)或需要在访问前后增加逻辑
  • 代理的三种常见分类
类型 说明 本案例
静态代理 编译期生成代理类,代码量大,扩展性差
JDK 动态代理 运行期通过 Proxy.newProxyInstance() + InvocationHandler 生成 ✅ 本案例使用
CGLIB 代理 运行期通过字节码增强生成子类,可代理无接口的类

二、标准体系结构图

角色 本案例对应 说明
Subject(抽象主题) IUserDao 接口 定义真实对象和代理对象的公共接口
RealSubject(真实主题) 无(本例没有真实实现类) 在 MyBatis 场景中,真正没有手写实现类
Proxy(代理) MapperFactoryBean 内的 InvocationHandler 运行期动态生成,拦截接口方法调用,执行 SQL
ProxyFactory(代理工厂) MapperFactoryBean + RegisterBeanFactory 负责创建代理对象并将其注册到 Spring 容器
Client(客户端) ApiTest 从 Spring 容器取出 IUserDao 直接调用,不感知代理存在

三、场景推演

3.1 静态代理

场景:支付系统日志统计

需求:

  • 有一个支付服务 PaymentService,提供 pay() 方法。
  • 我们希望在支付操作前后:
    • 打印日志 → 记录支付开始和结束时间
    • 不想修改原支付类的代码

特点:

  • 静态代理需要手动写代理类。
  • 每个真实对象都需要一个对应的代理类。

优点:

  • 简单直观,逻辑清晰
  • 可以控制访问、增加额外逻辑

缺点:

  • 每个真实对象都要写一个代理类 → 代码重复
  • 扩展性差,维护成本高

使用场景:

  • 对象固定、代理逻辑简单、不会频繁改变

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 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 提供 addsubtract 方法。
  • 希望在方法调用前后:
    • 统计执行耗时
    • 不想写重复的代理类 → 所有接口实现对象都可动态代理

特点:

  • 动态代理基于接口(JDK Proxy)或类(Cglib)
  • 在运行时生成代理对象,不需要手写

优点:

  • 无需手写代理类,统一代理处理
  • 易于扩展 → 任何实现接口的对象都可代理
  • 可插入日志、权限、缓存、性能统计等

缺点:

  • 只能代理接口(JDK Proxy)
  • 性能略低于静态代理(反射调用)

使用场景:

  • AOP(切面编程) → Spring 就是基于动态代理
  • 日志、权限、缓存、事务控制
  • 对象数量多或变化频繁时特别适合

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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?

1
2
3
4
5
6
7
8
9
10
11
普通接口调用链(需要实现类):
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 → 返回结果

本案例模拟的核心步骤:

  1. 定义接口 IUserDao,方法上加 @Select(sql)
  2. MapperFactoryBean 实现 FactoryBean<T>
    getObject() 中用 Proxy.newProxyInstance() 生成代理
    InvocationHandler 读取 @Select 注解的 SQL,模拟执行
  3. RegisterBeanFactory 实现 BeanDefinitionRegistryPostProcessor
    Spring 启动时,将 MapperFactoryBean 包装的 bean
    以 “userDao“ 为名注册到 Spring 容器
  4. 调用方通过 beanFactory.getBean("userDao", IUserDao.class)
    拿到的是代理对象,调用 queryUserInfo() 触发 invoke()

原因:代理模式在此处是一种框架/中间件级别的能力,其对比的不是”坏代码 vs 好代码”,而是展示了一种”没有代理机制时必须手写大量重复 DAO 实现类“与”有代理机制后只需定义接口“的本质差异。

image-20260524214841476

本案例实现的等效功能:

对比维度 传统 JDBC 方式 本案例代理方式
需要手写实现类? 是,每个 DAO 都要写 XxxDaoImpl 否,只定义接口
SQL 放在哪里? 硬编码在实现类中 @Select 注解在方法上
Spring 注入方式 直接注入实现类 bean 代理工厂动态生成代理对象注入
扩展新 DAO 接口 写新实现类 + 配置 bean 只需定义接口 + 加注解

4.2 架构图

image-20260524140150727


4.3 类图


4.4 时序图


4.5 代码分析

4.5.1 自定义注解 @Select(模拟 MyBatis 的 SQL 注解)

1
2
3
4
5
6
7
// agent/Select.java
@Documented
@Retention(RetentionPolicy.RUNTIME) // 运行期保留,否则代理中无法通过反射读取
@Target({ElementType.METHOD}) // 作用于方法级别
public @interface Select {
String value() default ""; // 存储 SQL 语句
}

4.5.2 DAO 接口 IUserDao(只定义接口,无实现类)

1
2
3
4
5
6
// 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 动态代理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 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 扩展点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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

1
2
3
4
5
<!-- spring-config.xml -->
<beans ...>
<!-- 只需要配置 RegisterBeanFactory,它会在启动时自动注册代理 bean -->
<bean id="userDao" class="com.likerhood.design.agent.RegisterBeanFactory"/>
</beans>

注意:这里配置的 bean class 是 RegisterBeanFactory(注册器),而不是 IUserDaoMapperFactoryBeanRegisterBeanFactory 实现了 BeanDefinitionRegistryPostProcessor,Spring 会在容器启动阶段特殊处理它,调用其 postProcessBeanDefinitionRegistry() 方法动态注册 userDao 代理 bean。


4.5.6 测试验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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 定义接口、加注解,框架自动处理
代码体积 随接口数量线性增长 框架代码固定,接口数量增加不增加框架代码

代理模式的关键实现要点:

  1. JDK 动态代理三要素:ClassLoader(指定代理类的类加载器)、Class[](代理要实现的接口列表)、InvocationHandler(所有方法调用的拦截器)——缺一不可。
  2. FactoryBean 的间接性:Spring 中 FactoryBean.getObject() 返回的才是真正注入的对象,这是代理模式与 Spring 结合的核心”技巧”,MyBatis-Spring、Dubbo 的 RPC 代理等都用了这个机制。
  3. BeanDefinitionRegistryPostProcessor 扩展点:这是框架动态注册 bean 的正规入口,比 XML 配置更灵活,批量注册时尤为强大(MyBatis-Spring 的 @MapperScan 底层正是扫描包内所有接口,循环注册每个 MapperFactoryBean)。

代理模式的适用场景:

场景 说明
MyBatis Mapper 接口代理,拦截方法调用执行 SQL
RPC 框架(Dubbo/gRPC) 接口代理,拦截调用发起远程网络请求
Spring AOP CGLIB/JDK 代理,切面拦截方法调用插入逻辑
权限控制代理 在调用前检查权限,通过再转发给真实对象
缓存代理 在调用前查缓存,未命中再转发并写缓存
延迟加载代理 对象使用时才真正初始化(Hibernate 懒加载)

与其他模式对比:

  • 代理模式 vs 装饰器模式:代理控制对目标对象的访问(可以拒绝、缓存、远程化),装饰器为目标对象添加功能。代理通常自己管理目标对象的生命周期,装饰器中目标对象由调用方传入
  • 代理模式 vs 适配器模式:适配器改变接口,代理不改变接口(接口完全相同,只是在调用链上插入一层)
  • 代理模式 vs 外观模式:外观对外提供简化接口(多个子系统 → 一个接口),代理是对单一对象的封装(一个接口 → 同一接口,透明)