谈谈 Mybatis 的工作原理?
结论先行:MyBatis 的骨架是「一个 Configuration + 一个 SqlSession 门面 + 一条执行链」。启动时把 XML/注解解析成 Configuration(内含 MappedStatement、ResultMap、TypeHandler 等);运行时由 SqlSession 委托 Executor,再经 StatementHandler 预编译、ParameterHandler 绑参、ResultSetHandler 映射结果。Mapper 接口没有实现类,靠 JDK 动态代理把方法调用翻译成 MappedStatement 的 id。
1. 启动阶段的解析
mybatis-config.xml Mapper.xml / @Select
│ │
▼ ▼
XMLConfigBuilder XMLMapperBuilder / MapperAnnotationBuilder
│ │
└──────────┬─────────────────┘
▼
Configuration
┌───────────────┼──────────────────┬─────────────────┐
│ Environment │ MappedStatement │ TypeHandlerRegistry│
│ 数据源/事务 │ 一条 SQL 的全部元数据│ JdbcType ↔ Java │
└───────────────┴──────────────────┴─────────────────┘
▼
SqlSessionFactory(单例,全局唯一)
关键对象:
| 对象 | 职责 |
|---|---|
Configuration | 全局配置中心,存放所有 MappedStatement、缓存、插件链 |
MappedStatement | 一条 SQL 的封装:id、SQL 源、参数映射、结果映射、缓存标记 |
SqlSessionFactory | 生产 SqlSession 的工厂,线程安全 |
SqlSession | 面向用户的会话门面,非线程安全,用后即关 |
Executor | 执行器,负责缓存、事务、SQL 执行的调度 |
2. 运行时的执行链
mapper.selectById(1L)
│
▼ JDK 代理 MapperProxy
MapperMethod.execute() ── 解析方法名 → statementId = namespace + "." + methodName
│
▼ SqlSession.selectOne(statementId, param)
DefaultSqlSession
│
▼ Executor.query(ms, param, rowBounds, handler)
CachingExecutor ── 二级缓存命中则直接返回
│
▼ SimpleExecutor.doQuery()
StatementHandler.prepare() ── Connection.prepareStatement(sql)
ParameterHandler.setParameters() ── ps.setXxx(i, value)
│
▼ ps.execute()
ResultSetHandler.handleResultSets() ── 反射映射成对象
│
▼ 返回 List / 单对象,关闭 Statement
源码里的入口非常直观:
// DefaultSqlSession
public <T> T selectOne(String statement, Object parameter) {
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) return list.get(0);
if (list.size() > 1) throw new TooManyResultsException(...);
return null;
}
3. 一次查询的代码视角
String resource = "mybatis-config.xml";
try (InputStream in = Resources.getResourceAsStream(resource)) {
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
try (SqlSession session = factory.openSession()) { // 默认不自动提交
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1L);
session.commit(); // 查询无需提交,写操作必须
System.out.println(user);
}
}
Spring 集成后这些样板被 SqlSessionTemplate 接管:它持有一个与事务同步的 SqlSession 代理,方法调用自动开闭会话、自动提交或回滚。
4. 缓存与插件在链路中的位置
| 阶段 | 介入点 |
|---|---|
| 查询入口 | Executor 被 CachingExecutor 装饰(二级缓存),再被插件代理 |
| 一级缓存 | BaseExecutor.query() 内先查 localCache,命中直接返回 |
| SQL 预编译 | StatementHandler.prepare(),ReuseExecutor 会复用 Statement |
| 参数绑定 | ParameterHandler.setParameters(),由 TypeHandler 完成赋值 |
| 结果映射 | ResultSetHandler.handleResultSets() |
| 事务提交 | Executor.commit() → Transaction.commit() |
5. 常见追问
追问一:SqlSession 为什么不是线程安全的?
它持有一个 Executor,Executor 内有 localCache、当前 Connection 和未提交事务状态。多线程共享会导致缓存串数据、连接被并发使用。Spring 通过「每个事务/每次操作一个 SqlSession」保证隔离。
追问二:SqlSessionFactory 为什么可以单例?
它只持有不可变的 Configuration(启动后不再修改),不保存任何会话状态,所以天然线程安全。这也是为什么它通常在应用启动时构建一次。
追问三:Mapper 接口调用和 sqlSession.selectOne 有什么区别?
本质相同。Mapper 接口是语法糖:代理对象根据「接口全限定名 + 方法名」找到 MappedStatement,再委托 SqlSession 执行。区别只在参数封装:多参数会被包成 ParamMap。
追问四:为什么 MyBatis 的 MappedStatement 用 id 而不是方法引用?
id 是字符串 namespace.methodName,让 XML 与接口解耦:同一个 XML 可以被不同接口复用,也支持 databaseId 做多数据库适配。
追问五:Executor 在什么时候创建?
openSession() 时由 Configuration.newExecutor() 创建,同时根据 ExecutorType 选择 Simple/Reuse/Batch,再用 CachingExecutor 包装并应用插件链。
小结
把 MyBatis 拆成两半就很好记:启动时把配置变成 Configuration 里的一堆元数据;运行时用 SqlSession 把这堆元数据串成 Executor → StatementHandler → ParameterHandler → ResultSetHandler 的执行链。Mapper 代理只是这条链的入口语法糖,缓存和插件都是在这条链上挂的钩子。