🧭 一句话总览
MyBatis 执行一条 SQL 的核心流程是:
SqlSession → MapperProxy → Executor → StatementHandler → ResultSetHandler
下面逐层拆解(附关键源码逻辑):
🔁 一、整体执行流程图(简化版)
[Mapper Interface]
↓ (JDK动态代理)
[MapperProxy.invoke()]
↓
[SqlSession.selectOne(...)]
↓
[Configuration.getMappedStatement()]
↓
[Executor.query()] ← 一级缓存在此生效
↓
[BaseExecutor.createCacheKey()]
↓
[是否命中一级缓存?] → 是 → 返回结果
↓ 否
[SimpleExecutor.doQuery()]
↓
[StatementHandler.prepare()] ← 获取 Connection + 预编译 SQL
↓
[ParameterHandler.setParameters()] ← 设置 #{} 参数(防注入!)
↓
[Statement.execute()] ← JDBC 执行
↓
[ResultSetHandler.handleResultSets()] ← 封装结果
↓
[返回 Java 对象]
🔍 二、关键步骤详解
1️⃣ 入口:调用 Mapper 接口方法
java
User user = userMapper.selectById(1L);
- 本质 :
userMapper是 MyBatis 通过 JDK 动态代理生成的代理对象。 - 代理逻辑 :所有方法调用都会进入
MapperProxy.invoke()。
💡 为什么能直接调接口?→ MyBatis 在启动时扫描
@Mapper或 XML,注册了代理工厂。
2️⃣ 路由到 SqlSession
MapperProxy 内部会调用:
java
sqlSession.selectOne("com.example.UserMapper.selectById", param);
- 方法名拼接成 全限定 ID :
namespace + "." + method name - 这个 ID 对应 XML 中
<select id="selectById">
3️⃣ 获取 MappedStatement
Configuration对象在 MyBatis 启动时就加载了所有 XML/注解,解析成MappedStatement。- 每个 SQL 语句(增删改查)都对应一个
MappedStatement,包含:- SQL 字符串
ResultMap- 超时设置
- 缓存配置等
4️⃣ 执行器(Executor)处理 ------ 一级缓存入口
-
默认使用
SimpleExecutor(还有ReuseExecutor,BatchExecutor) -
关键逻辑:
java// BaseExecutor.java public <E> List<E> query(...) { CacheKey key = createCacheKey(ms, param, ...); if (localCache.getObject(key) != null) { return (List<E>) localCache.getObject(key); // 👈 一级缓存命中 } return queryFromDatabase(...); // 否则查数据库 }
✅ 一级缓存是 SqlSession 级别的 HashMap,只要没 commit/close,相同查询只查一次 DB。
5️⃣ SQL 准备与参数绑定
-
创建
PreparedStatement(预编译,防 SQL 注入) -
通过
ParameterHandler将#{id}替换为?,并调用ps.setLong(1, 1L)java// DefaultParameterHandler public void setParameters(PreparedStatement ps) { // 遍历参数,调用 TypeHandler 转换并设置 typeHandler.setParameter(ps, i, value, jdbcType); }
🔑
#{}vs${}区别就在这里:
#{}→ 预编译参数(安全)${}→ 直接字符串拼接(危险!)
6️⃣ 执行 JDBC 并处理结果
- 调用
ps.executeQuery() - 通过
ResultSetHandler(默认DefaultResultSetHandler)遍历ResultSet - 根据
ResultMap或自动映射,将每一行数据转换为 Java 对象- 处理
<association>/<collection> - 处理驼峰转换(
mapUnderscoreToCamelCase)
- 处理
7️⃣ 返回结果
- 单条 → 返回对象
- 多条 → 返回
List - 插入 → 返回影响行数(或主键,如果配置了
useGeneratedKeys)
⚙️ 三、关键组件职责总结
| 组件 | 职责 |
|---|---|
| SqlSession | 用户操作入口,线程不安全 |
| MapperProxy | 动态代理,将接口调用转为 SqlSession 调用 |
| Executor | 执行器,负责缓存、事务、真正执行 SQL |
| StatementHandler | 封装 PreparedStatement,处理 SQL 准备 |
| ParameterHandler | 设置参数(#{} 绑定) |
| ResultSetHandler | 处理结果集,映射为 Java 对象 |
| TypeHandler | Java 类型 ↔ JDBC 类型转换(如 String ↔ VARCHAR) |
⚠️ 四、大厂关注点(面试加分项)
-
一级缓存失效场景:
- 执行了 DML(insert/update/delete)
- 调用了
clearCache() commit()/close()
-
为什么 SqlSession 不是线程安全的?
→ 因为它内部持有 数据库连接 + 一级缓存(HashMap),多线程会冲突。
-
MyBatis 如何防止 SQL 注入?
→ 预编译(PreparedStatement) +
#{}参数绑定 ,${}不安全!
✅ 五、总结
MyBatis 执行流程 = 代理 → 路由 → 缓存 → 预编译 → 执行 → 映射
理解这条链路,你就掌握了 MyBatis 的"脉络",无论是调优、debug 还是面试,都能游刃有余。
如果你正在用 Spring Boot,MyBatis 的 SqlSession 会被 Spring 托管,但底层流程不变。