Java 动态代理深度解析:从”为什么”到”底层原理” 前言 你有没有思考过一个问题:在 MyBatis 中,你只写了一个 UserMapper 接口,从来没有写过它的实现类,却可以直接调用 userMapper.selectById(1) 查出数据库结果?
这背后隐藏的技术叫做 JDK 动态代理 ,它是 Java 反射机制的核心应用之一,也是 MyBatis、Spring AOP、RPC 框架的基石。本文将从”为什么需要它”出发,逐步深入到底层字节码原理,并附上可运行的完整代码。
本文运行代码链接:https://github.com/likerhood/CodeDesignWork/tree/main/codedesign0.0-0/src/main/java/com/likerhood/design/mybatisproxy
一、为什么需要动态代理? 1.1 先从一个真实问题说起 你每天都在用 MyBatis,但有没有想过这段代码为什么能运行?
1 2 3 4 5 6 public interface UserMapper { String selectById (int id) ; void insert (String username) ; void deleteById (int id) ; }
1 2 3 UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class, sqlSession);String user = userMapper.selectById(1 );
整个项目里,你搜不到任何 UserMapperImpl 之类的实现类。一个没有实现类的接口,方法调用是怎么执行的?
1.2 没有动态代理时,你必须这么写 如果没有动态代理,要让 UserMapper 能用,唯一的办法是手动写实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class UserMapperImpl implements UserMapper { private SqlSession sqlSession = new SqlSession (); @Override public String selectById (int id) { 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,每一个都要写这样一个实现类,而且每个实现类的方法体几乎完全一样 ,只有方法名字符串不同:
1 2 3 4 return (String) sqlSession.execute("selectById" , new Object []{id});
这就是问题所在:
几十个 Mapper 就要写几十个实现类,代码高度重复
UserMapper 新增一个方法,UserMapperImpl 必须同步修改
所有实现类在编译期就写死了,完全是机械劳动
1.3 动态代理如何解决这个问题 观察上面的重复代码,你会发现规律——每个方法体做的事情完全一样:拿到方法名,调用 sqlSession.execute() 。
既然逻辑是固定模板,为什么要人工重复写?让 JDK 在运行时自动生成这些实现类就好了。
动态代理只需要你描述这个”固定模板”:
1 2 3 4 5 6 @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { return sqlSession.execute(method.getName(), args); }
然后 JDK 替你生成 UserMapperImpl、OrderMapperImpl… 所有实现类,每个方法体都套用这个模板。你什么都不用写。
动态代理解决的核心问题: 把”接口实现类的编写工作”从编译期的人工重复劳动,变成运行时由 JDK 自动完成。
二、三个核心概念是什么? 2.1 Java 反射(Reflection) 是什么: 在运行时”审视”和”操作”一个类的能力,包括获取它的方法、字段、构造器,并动态调用。
讲解反射的博客:https://likerhood.github.io/2026/05/09/032%20java%E5%8F%8D%E5%B0%84%E5%92%8C%E6%B3%A8%E8%A7%A3/
先问:为什么动态代理需要反射?
$Proxy0 生成之后,它的每个方法体里只有一行固定代码:
1 2 3 public String selectById (int id) { return (String) h.invoke(this , m_selectById, new Object []{id}); }
h.invoke() 需要告诉 MapperProxy:”你现在处理的是 selectById 这个方法,参数是 1“。但 invoke() 的签名是:
1 Object invoke (Object proxy, Method method, Object[] args)
method 这个参数就是反射的 Method 对象——它是一张方法名片 ,携带了被调用方法的所有元信息。没有反射,MapperProxy 根本不知道当前被调用的是哪个方法。
反射是什么: 在运行时”审视”一个类的能力——可以把任何一个方法包装成 Method 对象,通过它读取方法名、参数类型、返回类型,并动态调用。
1 2 3 4 5 6 String result = userMapper.selectById(1 );Method method = UserMapper.class.getMethod("selectById" , int .class);Object result = method.invoke(userMapper, 1 );
在本案例中反射做了什么:
1 2 3 4 5 6 return sqlSession.execute(method.getName(), args);
Method 对象能提供的信息:
1 2 3 4 5 6 7 Method method = UserMapper.class.getMethod("selectById" , int .class);method.getName() method.getParameterTypes() method.getReturnType() method.getDeclaringClass() method.getAnnotations()
常用 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:用这个管理员,把我动态生成的这本”新书”放进阅览室 。
1 2 3 4 5 6 7 8 9 10 11 ClassLoader cl = Thread.currentThread().getContextClassLoader();ClassLoader cl = UserMapper.class.getClassLoader();ClassLoader cl = ClassLoader.getSystemClassLoader();ClassLoader parent = cl.getParent();
2.3 JDK 动态代理 前两节解决了两个子问题:
反射解决了:运行时如何知道调用了哪个方法、如何动态转发
ClassLoader 解决了:动态生成的类如何合法地进入 JVM
动态代理就是把这两者整合起来的最终执行器 。
Proxy.newProxyInstance 三参数,每个对应一个子问题:
1 2 3 4 5 Proxy.newProxyInstance( classLoader, new Class []{UserMapper.class}, mapperProxy )
$Proxy0 内部结构——三者协作的汇聚点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public final class $Proxy0 extends Proxy implements UserMapper { private static Method m_selectById; static { m_selectById = UserMapper.class.getMethod("selectById" , int .class); } public $Proxy0(InvocationHandler h) { super (h); } @Override public String selectById (int id) { return (String) h.invoke(this , m_selectById, new Object []{id}); } }
三概念协作的完整时序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 你写的代码: 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() 的第二个参数
1 2 3 4 5 public static Object newProxyInstance ( ClassLoader loader, // 参数1 类加载器:来把 $Proxy0 加载进 JVM Class<?>[] interfaces, // 参数2 $Proxy0 要实现具体的接口?(决定它的类型) InvocationHandler h // 参数3 方法调用交给谁处理 )
三者的协作流程(对应本案例):
整体流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 第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 完整代码结构 1 2 3 4 5 mybatisproxy/ ├── UserMapper.java ← 接口(无实现类) ├── SqlSession.java ← 模拟数据库执行层 ├── MapperProxy.java ← InvocationHandler 实现(核心) └── MapperProxyFactory.java ← 代理工厂
3.2 定义接口 1 2 3 4 5 6 7 public interface UserMapper { String selectById (int id) ; void insert (String username) ; void deleteById (int id) ; }
3.3 实现 InvocationHandler 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class MapperProxy implements InvocationHandler { private SqlSession sqlSession; public MapperProxy (SqlSession sqlSession) { this .sqlSession = sqlSession; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this , args); } return sqlSession.execute(method.getName(), args); } }
invoke 三个参数的含义:
1 2 3 4 invoke(Object proxy, Method method, Object[] args)
3.4 创建代理工厂 1 2 3 4 5 6 7 8 9 10 11 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 模拟数据库执行层 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 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 测试代码 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 public class MyBatisProxyTest { @Test public void test_basicProxy () { SqlSession sqlSession = new SqlSession (); UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class, sqlSession); 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 )); } @Test public void test_proxyIdentity () { SqlSession sqlSession = new SqlSession (); UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class, sqlSession); System.out.println("真实类名: " + userMapper.getClass().getName()); System.out.println("instanceof UserMapper: " + (userMapper instanceof UserMapper)); } @Test public void test_reflectionMethod () throws Exception { Method method = UserMapper.class.getMethod("selectById" , int .class); System.out.println("方法名: " + method.getName()); System.out.println("参数类型: " + Arrays.toString(method.getParameterTypes())); System.out.println("返回类型: " + method.getReturnType()); System.out.println("所属接口: " + method.getDeclaringClass()); } @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)); } }
运行 test_basicProxy 输出:
1 2 3 4 5 6 执行 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 的远程调用客户端,它们的核心原理说都是同一件事。