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

很多人用了多年 MyBatis,只知道写 Mapper 接口、配 XML,却始终没搞懂: > 接口没有实现类,为什么能直接注入调用? > 一个方法调用,到底是怎么找到 XML 里的 SQL 并执行的? > 本文从JDK 动态代理、MapperProxy、Spring 注入、SQL 元数据查找、JDBC 执行全链路拆解,一次性把底层讲透,精准纠正动态代理调用细节,拒绝模糊表述。

一、开篇灵魂拷问

日常开发中,我们的代码长这样:

kotlin 复制代码
@Mapper
public interface UserMapper {
    User selectById(Long id);
}
xml 复制代码
<!-- UserMapper.xml -->
<mapper namespace="com.xxx.mapper.UserMapper">
    <select id="selectById" resultType="com.xxx.entity.User">
        select * from user where id = #{id}
    &lt;/select&gt;
&lt;/mapper&gt;

然后在 Service 里直接注入使用:

kotlin 复制代码
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    
    public User getById(Long id) {
        return userMapper.selectById(id);
    }
}

你一定会产生这些疑惑:

    1. UserMapper 是接口,不能实例化,Spring 为什么能注入?
  • 2.接口没有方法体,调用 selectById 时到底执行了什么代码?
    1. 方法名和 XML 中的 SQL 是怎么关联起来的?
    1. 底层到底是不是动态代理?用的 JDK 还是 Cglib?
    1. 代理类中 h.invoke 为什么要加 super.super.h 到底是什么?

本文一次性给出最本质、最接近源码、最精准的答案,重点纠正动态代理调用的核心细节。

二、核心结论先行

  1. MyBatis 采用JDK 动态代理 为所有 Mapper 接口生成代理对象,不存在手动编写的实现类。
  2. 所有 Mapper 接口的所有方法,最终都进入同一个类 MapperProxyinvoke 方法统一处理。
  3. Mapper 接口本身不包含任何逻辑,仅作用于:编译检查、IDEA 代码提示、调用入口规范。
  4. invoke 内部通过"接口全类名 + 方法名"定位 XML 中的 SQL 元数据,再执行底层 JDBC。
  5. Spring 注入的不是接口,而是内存中真实存在的代理实例,完全符合 Spring 依赖注入规则。

三、关键角色一览

在正式看流程前,先认识几个核心类,它们是 MyBatis Mapper 的灵魂:

类名 作用
MapperProxy 实现 InvocationHandler,所有 Mapper 的统一代理处理器,是所有方法调用的最终入口
MapperProxyFactory 用于创建 Mapper 代理对象的工厂,负责初始化 MapperProxy 并生成代理实例
MapperRegistry Mapper 注册中心,管理所有 Mapper 接口与代理生成,提供 getMapper 方法获取代理实例
MappedStatement XML 解析后的 SQL 元数据(SQL 文本、参数类型、返回类型、结果映射、节点类型)
Configuration MyBatis 全局配置,持有所有 MappedStatement,供 MapperProxy 查找 SQL 元数据
Proxy(JDK 自带) 所有 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);
        }
    }
}

重点强调(文章核心纠正点):

    1. 代理类 $ProxyXX继承自 java.lang.reflect.Proxy,不是直接实现接口。
    1. 父类 Proxy 中有一个 protected final InvocationHandler h 字段,用于保存代理处理器。
    1. 代理类的构造方法会将 MapperProxy 实例传入父类,赋值给 h
    1. 调用接口方法时,必须通过 super.h 访问父类的 h,进而调用MapperProxy.invoke
    1. 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 源码精简版(保留核心逻辑,去掉无关校验):

java 复制代码
public class MapperProxy<T> implements InvocationHandler {
    // 被代理的 Mapper 接口(如 UserMapper.class)
    private final Class<T> 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 方法执行:

javascript 复制代码
return (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 操作的完整流程:

  1. 获取数据库连接(从连接池获取)
  2. 构建 PreparedStatement,替换 SQL 中的 #{id} 为实际参数 1L
  3. 执行 SQL 查询,获取 ResultSet 结果集
  4. 将 ResultSet 映射为 User 对象(根据 MappedStatement 中的结果映射规则)
  5. 关闭连接、释放资源(连接池管理)

6. 返回结果到 Service

将映射好的 User 对象返回给 Service 层,完成整个调用流程。

用一张流程图锁死全链路:

七、为什么 Mapper 接口只是"语法糖"?

理解了上面的流程,你就能彻底明白:Mapper 接口本质上只是一套调用规范与命名约束,没有任何业务逻辑

它的真实作用只有 3 个:

  1. 编译期检查:调用不存在的方法、参数类型不匹配,编译时直接报错,避免运行时异常。
  2. IDEA 代码提示:方法补全、参数提示、跳转 XML,提升开发效率(没有接口,就没有这些提示)。
  3. 提供方法签名:为 MapperProxy 提供"接口全类名 + 方法名"的唯一标识,用于定位 XML 中的 SQL。

真正的业务逻辑,其实在两个地方:

  • XML 中:存储 SQL 语句和结果映射规则。
  • MapperProxy.invoke 中:统一调度逻辑,负责查找 SQL、执行 JDBC。

八、Spring 注入为什么不报错?(彻底解惑)

很多开发者有一个误区: "接口不能被注入,Spring 会报错"

错!Spring 的注入规则从来不是"不能注入接口",而是:

根据注入的类型(如 UserMapper),在 Spring 容器中查找"实现了该接口的实例对象",找到就注入,找不到才报错。

MyBatis 与 Spring 整合时,会通过 MapperScannerConfigurer 做三件事:

  1. 扫描 @Mapper 注解或 @MapperScan 配置的包,找到所有 Mapper 接口。
  2. 通过 MapperProxyFactory 为每个 Mapper 接口生成代理实例($ProxyXX)。
  3. 将代理实例注册到 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 动态代理。

十、全文最精髓总结

  1. MyBatis 用 JDK 动态代理为 Mapper 接口生成内存中的代理实例($ProxyXX),代理类继承自 Proxy,实现目标 Mapper 接口。
  2. 代理类中,super.h 就是 MyBatis 的 MapperProxy 实例,所有 Mapper 方法调用都会转发到 MapperProxy.invoke。
  3. invoke 方法通过"接口全类名 + 方法名"定位 XML 中的 SQL 元数据,再执行 JDBC 操作。
  4. Mapper 接口仅用于编译检查、代码提示,不包含任何业务逻辑,是一套"命名规范"。
  5. Spring 注入的是代理实例,不是接口,完全符合依赖注入规则,这也是接口能被注入的核心原因。

十一、结尾

MyBatis 的 Mapper 机制看起来神奇,本质其实非常简单: 用 JDK 动态代理统一调度,用"接口全类名 + 方法名"做 SQL 定位,用 XML 存储 SQL 元数据,最终落地到 JDBC 操作。

理解了 MapperProxy、super.h 的含义,以及整个调用链路,你就理解了 MyBatis 的一半灵魂------剩下的,就是 SQL 解析、结果映射、连接池管理等细节。

如果你用 MyBatis 多年,却一直没搞懂底层原理,这篇文章应该能帮你彻底打通任督二脉。

补充说明:本文基于 MyBatis 3.5.x 源码解析,核心逻辑适用于所有 MyBatis 3.x 版本,与 Spring Boot 整合时流程完全一致,无差异。

相关推荐
Mr.45672 小时前
Spring Boot 集成 PostgreSQL 表级备份与恢复实战
java·spring boot·后端·postgresql
LucianaiB2 小时前
王炸组合!腾讯云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!
后端
白露与泡影2 小时前
探索springboot程序打包docker的最佳方式
spring boot·后端·docker
开心就好20252 小时前
本地执行 IPA 混淆 无需上传致云端且不修改工程的方案
后端·ios
架构师沉默2 小时前
为什么一个视频能让全国人民同时秒开?
java·后端·架构
掘金码甲哥2 小时前
同样都是九年义务教育,他知道的AI算力科普好像比我多耶
后端
sthnyph3 小时前
SpringBoot Test详解
spring boot·后端·log4j
饼干哥哥3 小时前
搭建一个云端Skills系统,随时随地记录TikTok爆款
前端·后端
IT 行者3 小时前
LangChain4j 集成 Redis 向量存储:我踩过的坑和选型建议
java·人工智能·redis·后端