MyBatis 源码剖析
Java 高级 · 底层原理
MyBatis 以"半自动 ORM"著称,优雅之处不在封装了多少,而在于把 JDBC 的样板代码(建连接、预编译、设参、遍历 ResultSet、关资源)拆成一条可插拔的执行链。本文从 SqlSessionFactory 构建讲到 Mapper 代理、Executor 体系、参数/结果映射与插件责任链,让你不仅会用,还能写出自己的拦截器。
一、启动:SqlSessionFactory 与 Configuration
SqlSessionFactory 是全局单例的"工厂的工厂",由 SqlSessionFactoryBuilder.build() 构建,核心产物是 Configuration——它是 MyBatis 的"全局配置中枢",所有 MappedStatement、类型处理器、拦截器、二级缓存都挂在它上面。
// 构建流程: 读 XML → 解析成 Document → XMLConfigBuilder 填充 Configuration
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
// 内部: new XMLConfigBuilder(is, environment, props).parse() → 返回 ConfigurationConfiguration 里最关键的数据结构是 Map<String, MappedStatement> mappedStatements(key 是 namespace + id,即你调用 Mapper 方法时拼出的全限定名)。每个 MappedStatement 封装了一条 SQL 的元信息:SqlSource(SQL 来源)、ParameterMap、ResultMap、statementType(STATEMENT/PREPARED/CALLABLE)、flushCacheRequired、useCache 等。
TIP
MyBatis 是线程不安全的层级:SqlSessionFactory(全局单例)→ SqlSession(一次会话,线程不安全,用完必须 close)→ Executor(SqlSession 持有)→ StatementHandler 等。这和 Spring 整合后"为啥 Mapper 单例却安全"不矛盾——Spring 给每个 Mapper 注入的是代理,真正执行时从 SqlSessionTemplate 临时取 SqlSession(通过 TransactionSynchronizationManager 在同事务内复用)。
二、Mapper 接口代理:接口怎么变成可调用对象
你只写了接口没写实现类,MyBatis 却能 userMapper.selectById(1),秘密是 JDK 动态代理。
// MapperProxyFactory.newInstance 生成代理
public T newInstance(SqlSession sqlSession) {
MapperProxy<T> handler = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return Proxy.newProxyInstance(mapperInterface.getClassLoader(),
new Class[]{mapperInterface}, handler);
}
// MapperProxy.invoke: 把方法调用转成 MapperMethod 执行
public Object invoke(Object proxy, Method method, Object[] args) {
if (Object.class.equals(method.getDeclaringClass())) return method.invoke(this, args);
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args); // 内部按 CRUD 类型调 session 对应方法
}MapperMethod.execute 根据方法上的 @Select/XML 中的 SqlCommandType 决定调 sqlSession.selectOne/insert/...,并把方法名(接口全限定名)+ 参数拼成 MappedStatement 的 id,交给执行链。这样"调用接口方法"和"执行 SQL"就桥接起来了。
三、Executor:执行链的真正发动机
SqlSession 把活儿交给 Executor,它有三种实现:
| Executor | 行为 | 场景 |
|---|---|---|
SimpleExecutor | 每条 SQL 新建 Statement、用完即关 | 默认、简单 |
ReuseExecutor | 缓存 Statement(按 SQL 文本)复用 | 同会话反复执行同 SQL |
BatchExecutor | 批量 addBatch,攒批 executeBatch | 大批量写入 |
开启二级缓存时,会套一层 CachingExecutor(装饰器模式):先查二级缓存,未命中才委派内层 Executor,命中后回填。所以调用顺序是:CachingExecutor → (BaseExecutor) → StatementHandler。
3.1 参数与结果:TypeHandler 桥接 JDBC
ParameterHandler.setParameters:把 Java 参数按TypeHandler转成 JDBC 类型(StringTypeHandler→ps.setString,DateTypeHandler→ps.setTimestamp等)。#{}生成预编译占位符?,从根本上防 SQL 注入。ResultSetHandler.handleResultSets:按resultMap(或自动映射)把ResultSet列映射到 Java 对象。支持association(一对一)、collection(一对多)、嵌套resultMap、以及autoMapping驼峰映射(mapUnderscoreToCamelCase=true)。
WARNING
#{}与${}混用导致注入:${}是字符串直接拼接(无法预编译),只允许用于表名/排序字段(且必须白名单校验);查询条件一律#{}。- 一级缓存跨方法串数据:
SqlSession级别一级缓存,同会话重复查会命中;但多会话或事务隔离可能读到旧值。写多读少场景建议localCacheScope=STATEMENT(每次查询清缓存)。 resultMap字段名不匹配静默为空:下划线未开驼峰映射会映射失败。- Mapper 接口重载:XML 中
id必须唯一,接口重载但 SQL id 相同会冲突(MyBatis 不支持方法重载按参数区分)。
四、插件机制:责任链 + 动态代理
MyBatis 的四大可拦截点:Executor、StatementHandler、ParameterHandler、ResultSetHandler。插件基于责任链 + JDK 动态代理:InterceptorChain.pluginAll 把每个插件用 Plugin.wrap(动态代理)层层包裹目标对象。
// Plugin.wrap: 用 InvocationHandler 代理, 命中签名才走 intercept
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
return Proxy.newProxyInstance(type.getClassLoader(), interfaces,
new Plugin(target, interceptor, signatureMap));
}
// 自定义分页插件: 拦截 Executor.query
@Intercepts({@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation inv) throws Throwable {
MappedStatement ms = inv.getArgs()[0];
Object param = inv.getArgs()[1];
// 1. 取出原始 SQL 2. 拼 count 与 limit 3. 重写 BoundSql 替换 args[0]
// 4. 先查总数, 再改分页参数执行
return inv.proceed(); // 责任链继续向下
}
@Override public Object plugin(Object target) { return Plugin.wrap(target, this); }
}mybatis-config.xml 注册:<plugins><plugin interceptor="com.demo.PageInterceptor"/></plugins>。多个插件按配置顺序形成链,执行顺序与包裹顺序相反(最先包装的最后执行),调试时务必注意。
TIP
插件能拦 Executor 就拿到 MappedStatement 和 BoundSql,能拦 ParameterHandler 就改入参,能拦 ResultSetHandler 就改出参——这是 MyBatis 做分页、多租户隔离、逻辑删除、数据脱敏、SQL 审计的统一入口。但插件是全局生效、对性能有影响(每层代理一次方法调用),非必要不滥加。
五、缓存与一级/二级差异
- 一级缓存:
SqlSession级别,BaseExecutor.localCache的PerpetualCache。同会话内相同查询命中;update/commit/close会清空。默认开启,无法关闭(可降为 STATEMENT 级)。 - 二级缓存:
Mapper(namespace)级别,多个SqlSession共享,需<cache/>开启 + 实体类实现Serializable。读多写少、且数据变化不频繁时才有价值。分布式部署下二级缓存是本地内存,多节点不一致——生产请用 Redis 等集中缓存替代。
📚 官方文档与参考资料
- MyBatis 官方文档:https://mybatis.org/mybatis-3/zh/index.html
- MyBatis-Spring 整合文档:https://mybatis.org/spring/
- MyBatis 源码(GitHub):https://github.com/mybatis/mybatis-3
✏️ 练习与自测
- 画出
userMapper.selectById(1)从MapperProxy.invoke到ResultSetHandler返回对象的完整调用链,标注每一层职责。 - 写一个基于
@Intercepts的 SQL 执行耗时统计插件(拦截StatementHandler的update/query,用invocation.proceed()前后计时)。 - 解释一级缓存与二级缓存的作用范围、生命周期差异,并说明什么场景应把
localCacheScope设为STATEMENT。 - 读
InterceptorChain.pluginAll源码,解释多个插件叠加时intercept的实际执行顺序。
本节小结
MyBatis 的核心是一条"代理 → Executor → Handler"的执行链:MapperProxy 把接口调用转成 MappedStatement 执行;Executor 决定 Statement 策略(Simple/Reuse/Batch,外加 CachingExecutor 装饰);参数与结果都靠 TypeHandler 桥接 JDBC;Interceptor 用责任链在任意 Handler 上织入逻辑(分页/多租户/脱敏)。吃透执行链,你就能写出任何定制化数据访问能力。