文章目录
-
- 前言
- 一、为什么需要动态代理?
-
- [1.1 先从一个真实问题说起](#1.1 先从一个真实问题说起)
- [1.2 没有动态代理时,你必须这么写](#1.2 没有动态代理时,你必须这么写)
- [1.3 动态代理如何解决这个问题](#1.3 动态代理如何解决这个问题)
- 二、三个核心概念是什么?
-
- [2.1 Java 反射(Reflection)](#2.1 Java 反射(Reflection))
- [2.2 ClassLoader 类加载器](#2.2 ClassLoader 类加载器)
- [2.3 JDK 动态代理](#2.3 JDK 动态代理)
- [三、如何使用?--- 以模拟 MyBatis 为例](#三、如何使用?— 以模拟 MyBatis 为例)
-
- [3.1 完整代码结构](#3.1 完整代码结构)
- [3.2 定义接口](#3.2 定义接口)
- [3.3 实现 InvocationHandler](#3.3 实现 InvocationHandler)
- [3.4 创建代理工厂](#3.4 创建代理工厂)
- [3.5 模拟数据库执行层](#3.5 模拟数据库执行层)
- [3.6 测试代码](#3.6 测试代码)
- 总结
前言
你有没有思考过一个问题:在 MyBatis 中,你只写了一个 UserMapper 接口,从来没有写过它的实现类,却可以直接调用 userMapper.selectById(1) 查出数据库结果?
这背后隐藏的技术叫做 JDK 动态代理,它是 Java 反射机制的核心应用之一,也是 MyBatis、Spring AOP、RPC 框架的基石。本文将从"为什么需要它"出发,逐步深入到底层字节码原理,并附上可运行的完整代码。
一、为什么需要动态代理?
1.1 先从一个真实问题说起
你每天都在用 MyBatis,但有没有想过这段代码为什么能运行?
java
// UserMapper.java ------ 只有接口,没有任何实现类
public interface UserMapper {
String selectById(int id);
void insert(String username);
void deleteById(int id);
}
java
// 使用时直接调用,和普通对象没有任何区别
UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class, sqlSession);
String user = userMapper.selectById(1); // 能查出"张三"
整个项目里,你搜不到任何 UserMapperImpl 之类的实现类。一个没有实现类的接口,方法调用是怎么执行的?
1.2 没有动态代理时,你必须这么写
如果没有动态代理,要让 UserMapper 能用,唯一的办法是手动写实现类:
java
// 传统做法:手动实现接口
public class UserMapperImpl implements UserMapper {
private SqlSession sqlSession = new SqlSession();
@Override
public String selectById(int id) {
// 每个方法都要手动调 sqlSession,模板代码
return (String) sqlSession.execute("selectById", new Object[]{id});
}
@Override
public void insert(String username) {
sqlSession.execute("insert", new Object[]{username});
}
@Override
public void deleteById(int id) {
sqlSession.execute("deleteById", new Object[]{id});
}
}
现在只有一个 UserMapper,勉强可以接受。但真实项目里有 UserMapper、OrderMapper、ProductMapper... 几十个 Mapper,每一个都要写这样一个实现类,而且每个实现类的方法体几乎完全一样,只有方法名字符串不同:
java
// UserMapperImpl、OrderMapperImpl、ProductMapperImpl...
// 每个方法体都长这样,区别只有方法名字符串
return (String) sqlSession.execute("selectById", new Object[]{id});
// ↑ 唯一的区别
这就是问题所在:
- 几十个 Mapper 就要写几十个实现类,代码高度重复
UserMapper新增一个方法,UserMapperImpl必须同步修改- 所有实现类在编译期就写死了,完全是机械劳动
1.3 动态代理如何解决这个问题
观察上面的重复代码,你会发现规律------每个方法体做的事情完全一样:拿到方法名,调用 sqlSession.execute()。
既然逻辑是固定模板,为什么要人工重复写?让 JDK 在运行时自动生成这些实现类就好了。
动态代理只需要你描述这个"固定模板":
java
// MapperProxy.java ------ 描述"所有方法都应该怎么处理"
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 无论调用哪个方法,逻辑都一样:拿方法名 → 交给 sqlSession 执行
return sqlSession.execute(method.getName(), args);
}
然后 JDK 替你生成 UserMapperImpl、OrderMapperImpl... 所有实现类,每个方法体都套用这个模板。你什么都不用写。
动态代理解决的核心问题: 把"接口实现类的编写工作"从编译期的人工重复劳动,变成运行时由 JDK 自动完成。
二、三个核心概念是什么?
2.1 Java 反射(Reflection)
是什么: 在运行时"审视"和"操作"一个类的能力,包括获取它的方法、字段、构造器,并动态调用。
讲解反射的博客:https://likerhood.github.io/2026/05/09/032 java反射和注解/
先问:为什么动态代理需要反射?
$Proxy0 生成之后,它的每个方法体里只有一行固定代码:
java
public String selectById(int id) {
return (String) h.invoke(this, m_selectById, new Object[]{id});
}
h.invoke() 需要告诉 MapperProxy:"你现在处理的是 selectById 这个方法,参数是 1"。但 invoke() 的签名是:
java
Object invoke(Object proxy, Method method, Object[] args)
method 这个参数就是反射的 Method 对象------它是一张方法名片 ,携带了被调用方法的所有元信息。没有反射,MapperProxy 根本不知道当前被调用的是哪个方法。
反射是什么: 在运行时"审视"一个类的能力------可以把任何一个方法包装成 Method 对象,通过它读取方法名、参数类型、返回类型,并动态调用。
java
// 普通调用:编译期写死,调用哪个方法在编译时就确定了
String result = userMapper.selectById(1);
// 反射调用:运行时动态决定,效果完全等价
Method method = UserMapper.class.getMethod("selectById", int.class);
Object result = method.invoke(userMapper, 1);
在本案例中反射做了什么:
java
// MapperProxy.invoke() 里的核心一行
return sqlSession.execute(method.getName(), args);
// ↑
// method.getName() 从 Method 对象里读出方法名字符串
// 运行时才知道是 "selectById" 还是 "insert" 还是 "deleteById"
// 这就是为什么一个 invoke() 能处理所有方法------靠反射动态读名字
Method 对象能提供的信息:
java
Method method = UserMapper.class.getMethod("selectById", int.class);
method.getName() // "selectById" ← MapperProxy 用这个转发
method.getParameterTypes() // [int.class]
method.getReturnType() // String.class
method.getDeclaringClass() // interface UserMapper ← 用这个过滤 Object 方法
method.getAnnotations() // 注解数组 ← Spring AOP 用这个判断是否要增强
常用 API 速查:
| 关键 API | 作用 | 本案例使用位置 |
|---|---|---|
Class.forName("全类名") |
运行时按名字加载一个类 | --- |
clazz.getMethod("方法名", 参数类型) |
获取 public 方法的 Method 对象 | $Proxy0 静态块中 |
method.invoke(对象, 参数) |
动态调用方法 | MapperProxy.invoke() 核心 |
clazz.newInstance() |
反射创建实例(无参构造) | JDKProxyFactory 中 cacheAdapter.newInstance() |
method.getDeclaringClass() |
拿到方法所属的类 | invoke() 中过滤 Object 方法 |
2.2 ClassLoader 类加载器
先问:为什么动态代理需要 ClassLoader?
$Proxy0 是 JDK 在内存里动态生成的,磁盘上没有这个 .class 文件。JVM 只认识被加载进方法区的类,一个类如果没被加载,就根本不存在。
问题来了:普通类可以从磁盘读 .class 文件加载,$Proxy0 的字节码只在内存里,谁来把它加载进 JVM?
答案就是你传入的 ClassLoader。
ClassLoader 是什么: JVM 把字节码读入内存的"搬运工"。每个类都由某个 ClassLoader 负责加载,加载后以 Class 对象的形式存放在 JVM 方法区,你才能 new 出实例。
直接加载类和动态加载类的区别如图所示:

Java 中普通类与动态代理类(如 $Proxy0)在 JVM 底层运作机制上的三大核心差异:
- 出处不同(输入源) :
- 普通类 :来自静态编译期,作为
.class文件真实存在于磁盘上。 - 动态代理类 :没有实体文件,是在程序运行时由代码(
ProxyGenerator)在内存里临时拼凑生成的字节数组(byte[])。
- 普通类 :来自静态编译期,作为
- 加载路径不同(类加载器) :
- 两者都要接受"双亲委派"机制的审查,但加载动作有别。
- 普通类 :使用
loadClass()进行常规的磁盘 I/O 读取。 - 动态代理类 :因为只存在于内存,只能调用底层的
defineClass()方法,将字节数组强行注入给加载器以获得合法身份。
- 诞生方式不同(JVM 内存) :
- 它们最终都会在"方法区"注册为
Class元数据。但在"堆内存"中生成具体实例时: - 普通类 :通过熟悉的
new关键字直接创建。 - 动态代理类 :只能通过反射(
newInstance)强行创建,并在其内部偷偷塞入一个核心的拦截器(InvocationHandler)。
- 它们最终都会在"方法区"注册为
加载任何一个类时,都先问父加载器"你有没有这个类",父加载器找不到才自己加载。这叫双亲委派 ,目的是防止你自己写一个 java.lang.String 去替换核心类。
动态代理中 ClassLoader 的特殊角色:
普通类的加载流程:磁盘有 .class 文件 → ClassLoader 读取 → 加载进方法区。
但 $Proxy0 根本没有 .class 文件,它的字节码是 Proxy 在内存中动态生成的:

这就是为什么 Proxy.newProxyInstance 需要传入 classLoader------它要告诉 JVM:用这个管理员,把我动态生成的这本"新书"放进阅览室。
java
// 获取当前线程的类加载器(动态代理常用,能加载业务类)
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 获取某个类是由哪个 ClassLoader 加载的
ClassLoader cl = UserMapper.class.getClassLoader();
// 获取系统类加载器(= AppClassLoader)
ClassLoader cl = ClassLoader.getSystemClassLoader();
// 获取父加载器(AppClassLoader → ExtClassLoader → null[BootstrapCL])
ClassLoader parent = cl.getParent();
2.3 JDK 动态代理
前两节解决了两个子问题:
- 反射解决了:运行时如何知道调用了哪个方法、如何动态转发
- ClassLoader 解决了:动态生成的类如何合法地进入 JVM
动态代理就是把这两者整合起来的最终执行器。
Proxy.newProxyInstance 三参数,每个对应一个子问题:
java
Proxy.newProxyInstance(
classLoader, // → 交给 ClassLoader 解决"如何加载 $Proxy0"
new Class[]{UserMapper.class}, // → 告诉 ProxyGenerator 生成什么结构的类
mapperProxy // → 交给反射和 InvocationHandler 解决"方法如何转发"
)
$Proxy0 内部结构------三者协作的汇聚点:
java
// ProxyGenerator 生成的 $Proxy0(反编译后结构)
public final class $Proxy0 extends Proxy implements UserMapper {
// ① 反射在这里:静态块缓存每个方法的 Method 对象
private static Method m_selectById;
static {
m_selectById = UserMapper.class.getMethod("selectById", int.class);
}
// ② ClassLoader 在这里:这个类的字节码由 classLoader.defineClass() 加载进 JVM
// ③ InvocationHandler 在这里:构造时注入,每个方法体调用 h.invoke()
public $Proxy0(InvocationHandler h) {
super(h); // h 存入父类 Proxy 的 protected 字段
}
@Override
public String selectById(int id) {
// 方法体固定模板:把"谁、什么方法、什么参数"打包扔给 h
return (String) h.invoke(this, m_selectById, new Object[]{id});
// ↑ h = MapperProxy,invoke() 里用反射拿方法名转发
}
}
三概念协作的完整时序:
你写的代码:
UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class, sqlSession)
↓
Proxy.newProxyInstance(classLoader,
[UserMapper.class], ← 接口
mapperProxy) ← 拦截器
↓
JVM 内存中生成 $Proxy0,由 classLoader 加载
↓
返回 $Proxy0 实例,赋值给 userMapper
你调用:
userMapper.selectById(1)
↓
$Proxy0.selectById(1) ← 实际执行的是动态生成的代理类
↓
MapperProxy.invoke(proxy, method[selectById], args[1]) ← InvocationHandler 拦截
↓
sqlSession.execute("selectById", [1]) ← 反射拿到方法名,转发给真正的执行层
↓
返回 "张三"
三个核心角色的分工:
| 角色 | 类 | 职责 | 本案例对应 |
|---|---|---|---|
| 代理工厂 | java.lang.reflect.Proxy |
生成字节码、加载类、实例化 | MapperProxyFactory 调用它 |
| 拦截器接口 | java.lang.reflect.InvocationHandler |
定义"所有方法被调用时做什么" | MapperProxy 实现它 |
| 方法元信息 | java.lang.reflect.Method |
携带被拦截方法的所有信息 | invoke() 的第二个参数 |
java
public static Object newProxyInstance(
ClassLoader loader, // 参数1 类加载器:来把 $Proxy0 加载进 JVM
Class<?>[] interfaces, // 参数2 $Proxy0 要实现具体的接口?(决定它的类型)
InvocationHandler h // 参数3 方法调用交给谁处理
)
三者的协作流程(对应本案例):
整体流程:
第1步:生成字节码
ProxyGenerator.generateProxyClass(
"$Proxy0",
new Class[]{UserMapper.class}
)
→ 在内存中生成 byte[] 字节码
→ 内容就是上面那个伪代码的二进制形式
第2步:加载进JVM
classLoader.defineClass("$Proxy0", byteCode)
→ $Proxy0 的 Class 对象进入 JVM 方法区
→ 此刻 $Proxy0 作为一个"类"正式存在于 JVM 中
第3步:实例化
Constructor c = $Proxy0.getConstructor(InvocationHandler.class)
c.newInstance(mapperProxy)
→ 创建 $Proxy0 实例,把 mapperProxy 注入到 h 字段
第4步:返回
return ($Proxy0实例)
→ 强转为 UserMapper,赋值给 mapper 变量------帮我把这个流程可视化,生成美观直观的xml代码

缺少任何一个都不行:没有 ClassLoader,生成的类没人加载;没有接口,不知道要生成哪些方法;没有 Handler,生成了方法却没有任何逻辑。
三、如何使用?--- 以模拟 MyBatis 为例
3.1 完整代码结构
mybatisproxy/
├── UserMapper.java ← 接口(无实现类)
├── SqlSession.java ← 模拟数据库执行层
├── MapperProxy.java ← InvocationHandler 实现(核心)
└── MapperProxyFactory.java ← 代理工厂
3.2 定义接口
java
// UserMapper.java
public interface UserMapper {
String selectById(int id);
void insert(String username);
void deleteById(int id);
}
// 注意:没有任何实现类!
3.3 实现 InvocationHandler
java
// MapperProxy.java ------ 核心:所有方法调用的拦截器
public class MapperProxy implements InvocationHandler {
private SqlSession sqlSession; // 真正执行 SQL 的组件
public MapperProxy(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 过滤 toString/hashCode 等 Object 自带方法
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 核心:用方法名 + 参数 转发给 SqlSession 执行
return sqlSession.execute(method.getName(), args);
}
}
invoke 三个参数的含义:
java
invoke(Object proxy, Method method, Object[] args)
// ↓ ↓ ↓
// 代理对象本身 被调用方法的元信息 传入的参数
// ($Proxy0) (selectById) ([1])
3.4 创建代理工厂
java
// MapperProxyFactory.java
public class MapperProxyFactory {
public static <T> T getMapper(Class<T> mapperClass, SqlSession sqlSession) {
MapperProxy mapperProxy = new MapperProxy(sqlSession);
return (T) Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(), // ① 类加载器
new Class[]{mapperClass}, // ② 要实现的接口
mapperProxy // ③ 拦截器
);
}
}
3.5 模拟数据库执行层
java
// SqlSession.java
public class SqlSession {
private static Map<Integer, String> database = new HashMap<>();
static {
database.put(1, "张三");
database.put(2, "李四");
database.put(3, "王五");
}
public Object execute(String methodName, Object[] args) {
if ("selectById".equals(methodName)) {
int id = (int) args[0];
System.out.println("执行 SQL: SELECT * FROM user WHERE id = " + id);
return database.get(id);
}
if ("insert".equals(methodName)) {
int newId = database.size() + 1;
database.put(newId, (String) args[0]);
System.out.println("执行 SQL: INSERT INTO user VALUES (" + newId + ", '" + args[0] + "')");
return null;
}
if ("deleteById".equals(methodName)) {
database.remove((int) args[0]);
System.out.println("执行 SQL: DELETE FROM user WHERE id = " + args[0]);
return null;
}
throw new RuntimeException("未知方法: " + methodName);
}
}
3.6 测试代码
java
public class MyBatisProxyTest {
/**
* 测试1:核心流程验证
* 验证:调用方式和有实现类时完全一样,但背后走的是动态代理
*/
@Test
public void test_basicProxy() {
SqlSession sqlSession = new SqlSession();
// getMapper 返回的是 $Proxy0,不是任何手写实现类
UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class, sqlSession);
// 看起来普通,实际上每次调用都经过 MapperProxy.invoke()
String user = userMapper.selectById(1);
System.out.println("查到的用户: " + user); // 张三
userMapper.insert("赵六");
System.out.println("插入后查询: " + userMapper.selectById(4)); // 赵六
userMapper.deleteById(2);
System.out.println("删除后查询: " + userMapper.selectById(2)); // null
}
/**
* 测试2:验证代理对象的真实身份
* 关键:证明 userMapper 不是任何手写类,而是 JDK 动态生成的 $Proxy0
*/
@Test
public void test_proxyIdentity() {
SqlSession sqlSession = new SqlSession();
UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class, sqlSession);
// $Proxy0 的真实类名
System.out.println("真实类名: " + userMapper.getClass().getName());
// 输出: com.sun.proxy.$Proxy0
// 虽然是 $Proxy0,但确实实现了 UserMapper 接口
System.out.println("instanceof UserMapper: " + (userMapper instanceof UserMapper));
// 输出: true
// 但它不是任何手写实现类
// System.out.println(userMapper instanceof UserMapperImpl); // 编译报错,该类根本不存在
}
/**
* 测试3:反射 Method 对象的能力
* 展示 invoke() 中 method 参数能告诉你什么信息
*/
@Test
public void test_reflectionMethod() throws Exception {
Method method = UserMapper.class.getMethod("selectById", int.class);
System.out.println("方法名: " + method.getName());
// 输出: selectById
System.out.println("参数类型: " + Arrays.toString(method.getParameterTypes()));
// 输出: [int]
System.out.println("返回类型: " + method.getReturnType());
// 输出: class java.lang.String
System.out.println("所属接口: " + method.getDeclaringClass());
// 输出: interface com.likerhood.design.mybatisproxy.UserMapper
}
/**
* 测试4:ClassLoader 加载动态代理类的过程
* 展示 $Proxy0 是被哪个 ClassLoader 加载的
*/
@Test
public void test_classLoader() {
SqlSession sqlSession = new SqlSession();
UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class, sqlSession);
ClassLoader proxyClassLoader = userMapper.getClass().getClassLoader();
ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println("代理类的 ClassLoader: " + proxyClassLoader);
System.out.println("当前线程的 ClassLoader: " + appClassLoader);
System.out.println("是否同一个: " + (proxyClassLoader == appClassLoader));
// 输出: true ------ $Proxy0 由 AppClassLoader 加载,和普通类一样
}
}
运行 test_basicProxy 输出:
执行 SQL: SELECT * FROM user WHERE id = 1
查到的用户: 张三
执行 SQL: INSERT INTO user VALUES (4, '赵六')
插入后查询: 赵六
执行 SQL: DELETE FROM user WHERE id = 2
删除后查询: null
总结
动态代理并非魔法,而是三种 Java 底层技术的组合拳,每一层都有明确分工:
| 技术 | 解决的问题 | 在动态代理中的角色 |
|---|---|---|
| 反射 | 运行时如何知道调用了哪个方法 | 把方法包装成 [Method] 动态转发 |
| ClassLoader | 内存中生成的类如何进入 JVM | 通过 defineClass() 将动态字节码注册进方法区,赋予 $Proxy0 合法身份 |
| 动态代理 | 如何消除重复的接口实现类 | 整合前两者,运行时自动生成 $Proxy0,所有方法体转交 [InvocationHandler]处理 |
三者的依赖关系是单向的:动态代理依赖 ClassLoader 加载类,依赖反射传递方法信息,彼此分工而不耦合。
回到最初的问题:为什么 UserMapper 没有实现类却能调用?
Proxy.newProxyInstance\]在运行时替你写了 `UserMapperImpl`,叫做 `$Proxy0`。它的每个方法体只有一行------\[h.invoke()\],把调用转发给你的 `MapperProxy`,再由 `MapperProxy` 用反射读出方法名,交给 `SqlSession` 执行 SQL。你感知不到任何代理的存在,这正是动态代理的价值所在。
掌握这套机制之后,MyBatis 的 MapperProxy、Spring AOP 的事务切面、Dubbo 的远程调用客户端,它们的核心原理说都是同一件事。