谈谈 Mybatis 的工作原理?

#Java#面试八股

谈谈 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 代理只是这条链的入口语法糖,缓存和插件都是在这条链上挂的钩子。

笔记加载中…