MyBatis Mapper 实现原理彻底解密——从动态代理到 JDBC 执行全链路剖析

张开发
2026/4/15 12:51:41 15 分钟阅读

分享文章

MyBatis Mapper 实现原理彻底解密——从动态代理到 JDBC 执行全链路剖析
很多人用了多年 MyBatis只知道写 Mapper 接口、配 XML却始终没搞懂 接口没有实现类为什么能直接注入调用 一个方法调用到底是怎么找到 XML 里的 SQL 并执行的 本文从JDK 动态代理、MapperProxy、Spring 注入、SQL 元数据查找、JDBC 执行全链路拆解一次性把底层讲透精准纠正动态代理调用细节拒绝模糊表述。一、开篇灵魂拷问日常开发中我们的代码长这样kotlinMapper public interface UserMapper { User selectById(Long id); }xml!-- UserMapper.xml -- mapper namespacecom.xxx.mapper.UserMapper select idselectById resultTypecom.xxx.entity.User select * from user where id #{id} lt;/selectgt; lt;/mappergt;然后在 Service 里直接注入使用kotlinService public class UserService { Autowired private UserMapper userMapper; public User getById(Long id) { return userMapper.selectById(id); } }你一定会产生这些疑惑UserMapper 是接口不能实例化Spring 为什么能注入2.接口没有方法体调用 selectById 时到底执行了什么代码方法名和 XML 中的 SQL 是怎么关联起来的底层到底是不是动态代理用的 JDK 还是 Cglib代理类中 h.invoke 为什么要加 super.super.h 到底是什么本文一次性给出最本质、最接近源码、最精准的答案重点纠正动态代理调用的核心细节。二、核心结论先行MyBatis 采用JDK 动态代理为所有 Mapper 接口生成代理对象不存在手动编写的实现类。所有 Mapper 接口的所有方法最终都进入同一个类 MapperProxy 的 invoke 方法统一处理。Mapper 接口本身不包含任何逻辑仅作用于编译检查、IDEA 代码提示、调用入口规范。invoke 内部通过“接口全类名 方法名”定位 XML 中的 SQL 元数据再执行底层 JDBC。Spring 注入的不是接口而是内存中真实存在的代理实例完全符合 Spring 依赖注入规则。三、关键角色一览在正式看流程前先认识几个核心类它们是 MyBatis Mapper 的灵魂类名作用MapperProxy实现 InvocationHandler所有 Mapper 的统一代理处理器是所有方法调用的最终入口MapperProxyFactory用于创建 Mapper 代理对象的工厂负责初始化 MapperProxy 并生成代理实例MapperRegistryMapper 注册中心管理所有 Mapper 接口与代理生成提供 getMapper 方法获取代理实例MappedStatementXML 解析后的 SQL 元数据SQL 文本、参数类型、返回类型、结果映射、节点类型ConfigurationMyBatis 全局配置持有所有 MappedStatement供 MapperProxy 查找 SQL 元数据ProxyJDK 自带所有 JDK 动态代理类的父类内部持有 InvocationHandler 实例 h四、JDK 动态代理基础代理类的真实结构MyBatis 全程只使用JDK 动态代理因为 Mapper 是接口——JDK 动态代理天生就是为接口设计的而 CGLIB 是通过继承类实现代理不适合接口代理。JDK 动态代理的核心能力是在运行时动态生成字节码构造一个继承自 Proxy、实现了目标接口的代理类并加载到内存Metaspace中生成真实的实例对象。这个代理类如 $Proxy12的真实逻辑结构如下精准版scala// JDK 动态生成的代理类继承自 Proxy实现 UserMapper 接口 public final class $Proxy12 extends Proxy implements UserMapper { // 构造方法由 JDK 动态生成传入 InvocationHandler 实例 public $Proxy12(InvocationHandler h) { super(h); // 将 h 赋值给父类 Proxy 的 h 字段 } // 实现 UserMapper 接口的 selectById 方法 Override public User selectById(Long id) { try { // 关键修正必须用 super.h获取父类 Proxy 中的 h 实例 // 这个 h 就是 MyBatis 的 MapperProxy 实例 return (User) super.h.invoke( this, // 当前代理对象 Method对象, // selectById 方法的 Method 实例 new Object[]{id} // 方法参数 ); } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } }重点强调文章核心纠正点代理类 $ProxyXX继承自 java.lang.reflect.Proxy不是直接实现接口。父类 Proxy 中有一个 protected final InvocationHandler h 字段用于保存代理处理器。代理类的构造方法会将 MapperProxy 实例传入父类赋值给 h。调用接口方法时必须通过 super.h 访问父类的 h进而调用MapperProxy.invoke。super.h 就是 MyBatis 的 MapperProxy 实例——这是所有 Mapper 方法的最终调度者。补充验证你可以通过代码打印代理对象的相关信息亲眼看到这个关系scss// 打印代理对象的类名 System.out.println(userMapper.getClass()); // 输出class com.sun.proxy.$Proxy12 // 打印代理对象的父类 System.out.println(userMapper.getClass().getSuperclass()); // 输出class java.lang.reflect.Proxy // 验证是否实现了 UserMapper 接口 System.out.println(userMapper instanceof UserMapper); // 输出true这证明代理对象是真实存在的 Java 对象实现了 UserMapper 接口继承自 Proxy完全符合 Spring 注入规则。五、全局统一入口MapperProxy 的 invoke 方法源码级解析MyBatis 为所有 Mapper 接口提供了统一的代理处理类——MapperProxy所有 Mapper 的所有方法最终都会走到它的 invoke 方法没有例外。以下是 MyBatis 源码精简版保留核心逻辑去掉无关校验javapublic class MapperProxyT implements InvocationHandler { // 被代理的 Mapper 接口如 UserMapper.class private final ClassT mapperInterface; // MyBatis 会话用于执行 SQL private final SqlSession sqlSession; // MyBatis 全局配置持有所有 XML 解析后的 SQL 元数据 private final Configuration config; // 所有 Mapper 方法的统一入口 Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 1. 构建 SQL 唯一标识接口全类名 方法名对应 XML 的 namespace id String statementId mapperInterface.getName() . method.getName(); // 示例statementId com.xxx.mapper.UserMapper.selectById // 2. 从全局配置中查找 XML 解析好的 SQL 元数据MappedStatement MappedStatement ms config.getMappedStatement(statementId); // 3. 执行 SQL内部封装了 JDBC 操作 // 根据 SQL 类型select/insert/update/delete调用对应的执行方法 if (ms.getSqlCommandType() SqlCommandType.SELECT) { return sqlSession.selectOne(statementId, args); } else if (ms.getSqlCommandType() SqlCommandType.INSERT) { return sqlSession.insert(statementId, args); } // 其他 SQL 类型update/delete同理 return null; } }核心逻辑总结不管你是 UserMapper、OrderMapper、GoodsMapper不管你调用的是 selectById、insert、update全部走到这同一个 invoke 方法通过“接口全类名 方法名”定位到 XML 中的 SQL再执行 JDBC 操作。六、完整调用链路从 Service 一行代码到数据库执行我们以 userMapper.selectById(1L); 为例完整走一遍真实运行流程每一步都对应底层真实逻辑1. Service 调用方法开发者写的代码userMapper.selectById(1L); 看似调用的是 UserMapper 接口方法实际上调用的是JDK 动态生成的代理对象 $Proxy12 的 selectById 方法因为 Spring 注入的是代理实例。2. 代理类方法转发代理类 $Proxy12 的 selectById 方法执行javascriptreturn (User) super.h.invoke(this, method, new Object[]{1L});这里的 super.h 就是 MyBatis 初始化好的 MapperProxy 实例相当于把方法调用“转发”给 MapperProxy 处理。3. 进入 MapperProxy.invoke 方法MapperProxy 拿到三个关键参数proxy当前代理对象$Proxy12 实例method当前调用的方法selectById 的 Method 实例args方法参数[1L]第一步构建唯一标识statementId com.xxx.mapper.UserMapper.selectById。4. 查找 SQL 元数据MyBatis 启动时已经将所有 XML 文件解析为 MappedStatement并存储在 Configuration 中。 通过 statementId 从 Configuration 中取出对应的 MappedStatement里面包含SQL 文本select * from user where id #{id}参数类型Long返回类型com.xxx.entity.User结果映射规则将数据库字段映射到 User 类的属性5. 执行 JDBC 操作SqlSession 内部会封装 JDBC 操作的完整流程获取数据库连接从连接池获取构建 PreparedStatement替换 SQL 中的 #{id} 为实际参数 1L执行 SQL 查询获取 ResultSet 结果集将 ResultSet 映射为 User 对象根据 MappedStatement 中的结果映射规则关闭连接、释放资源连接池管理6. 返回结果到 Service将映射好的 User 对象返回给 Service 层完成整个调用流程。用一张流程图锁死全链路七、为什么 Mapper 接口只是“语法糖”理解了上面的流程你就能彻底明白Mapper 接口本质上只是一套调用规范与命名约束没有任何业务逻辑。它的真实作用只有 3 个编译期检查调用不存在的方法、参数类型不匹配编译时直接报错避免运行时异常。IDEA 代码提示方法补全、参数提示、跳转 XML提升开发效率没有接口就没有这些提示。提供方法签名为 MapperProxy 提供“接口全类名 方法名”的唯一标识用于定位 XML 中的 SQL。真正的业务逻辑其实在两个地方XML 中存储 SQL 语句和结果映射规则。MapperProxy.invoke 中统一调度逻辑负责查找 SQL、执行 JDBC。八、Spring 注入为什么不报错彻底解惑很多开发者有一个误区“接口不能被注入Spring 会报错”错Spring 的注入规则从来不是“不能注入接口”而是根据注入的类型如 UserMapper在 Spring 容器中查找“实现了该接口的实例对象”找到就注入找不到才报错。MyBatis 与 Spring 整合时会通过 MapperScannerConfigurer 做三件事扫描 Mapper 注解或 MapperScan 配置的包找到所有 Mapper 接口。通过 MapperProxyFactory 为每个 Mapper 接口生成代理实例$ProxyXX。将代理实例注册到 Spring 容器中Bean 的类型就是对应的 Mapper 接口类型。所以当你写 Autowired private UserMapper userMapper; 时Spring 会在容器中找到“实现了 UserMapper 接口的代理实例”直接注入——完全符合 Spring 依赖注入规范不会报错。九、常见误区纠正避坑重点误区 1“MyBatis 为每个 Mapper 生成不同的代理处理器”——错所有 Mapper 共用同一个 MapperProxy 类不同 Mapper 对应不同的 MapperProxy 实例持有不同的 mapperInterface。误区 2“$ProxyXX 类不存在是虚拟的”——错它是 JDK 动态生成的真实类有字节码、有 Class 对象、有实例存在于内存中可被 GC 回收。误区 3“代理类中的 h.invoke 不需要加 super”——错h 是父类 Proxy 的字段子类必须用 super.h 访问否则会报错。误区 4“MyBatis 可能用 CGLIB 代理 Mapper”——错Mapper 是接口CGLIB 是继承类实现代理无法代理接口MyBatis 全程只用 JDK 动态代理。十、全文最精髓总结MyBatis 用 JDK 动态代理为 Mapper 接口生成内存中的代理实例$ProxyXX代理类继承自 Proxy实现目标 Mapper 接口。代理类中super.h 就是 MyBatis 的 MapperProxy 实例所有 Mapper 方法调用都会转发到 MapperProxy.invoke。invoke 方法通过“接口全类名 方法名”定位 XML 中的 SQL 元数据再执行 JDBC 操作。Mapper 接口仅用于编译检查、代码提示不包含任何业务逻辑是一套“命名规范”。Spring 注入的是代理实例不是接口完全符合依赖注入规则这也是接口能被注入的核心原因。十一、结尾MyBatis 的 Mapper 机制看起来神奇本质其实非常简单用 JDK 动态代理统一调度用“接口全类名 方法名”做 SQL 定位用 XML 存储 SQL 元数据最终落地到 JDBC 操作。理解了 MapperProxy、super.h 的含义以及整个调用链路你就理解了 MyBatis 的一半灵魂——剩下的就是 SQL 解析、结果映射、连接池管理等细节。如果你用 MyBatis 多年却一直没搞懂底层原理这篇文章应该能帮你彻底打通任督二脉。补充说明本文基于 MyBatis 3.5.x 源码解析核心逻辑适用于所有 MyBatis 3.x 版本与 Spring Boot 整合时流程完全一致无差异。

更多文章