简单聊聊 MyBatis 源码
简单聊聊 MyBatis 源码
这篇基于 MyBatis 3.5.x + mybatis-spring 2.x + mybatis-spring-boot-starter 2.3.x(对应 Spring Boot 2.7)分析。
MyBatis 是半自动 ORM 框架,核心只干一件事:把接口和 SQL 关联起来,运行时用动态代理把"接口方法调用"变成"JDBC 执行"。
主线分四层:配置解析(Configuration)→ 会话构建(SqlSession)→ Mapper 代理(MapperProxy)→ SQL 执行(Executor + StatementHandler)。
最后讲它怎么"零配置"整合进 SpringBoot——原理链是 starter → 自动装配 → FactoryBean → SqlSessionTemplate,和 Feign 的整合套路同源。
一、一句话总览
MyBatis 干的事概括成一句话:启动时把 XML/注解解析成 Configuration(每条 SQL 一个 MappedStatement),运行时 Mapper 接口由 JDK 动态代理接管,方法调用转成 SqlSession 的一次 JDBC 执行。
整体架构和一条完整调用链:
配置解析层: mybatis-config.xml + mapper.xml + 注解
│ XmlConfigBuilder / XmlMapperBuilder
▼
配置中枢: Configuration(mappedStatements: Map<id, MappedStatement>)
│ SqlSessionFactoryBuilder.build()
▼
会话层: SqlSessionFactory ──▶ SqlSession(DefaultSqlSession)
│ getMapper()
▼
代理层: MapperProxy(JDK 动态代理, InvocationHandler)
│ MapperMethod.execute()
▼
执行层: Executor(一/二级缓存) → StatementHandler(参数/语句) → JDBC
│
▼
映射层: ResultSetHandler → 反射创建 POJO + TypeHandler 类型转换
面试点:MyBatis 的"半自动"体现在 SQL 自己写、映射靠约定/XML;对比 Hibernate 的"全自动"(HQL 生成 SQL、对象状态管理)。所以 MyBatis 灵活、可控、适合复杂 SQL 场景。
二、配置解析:一切都进 Configuration
1. 构建入口
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
build() 内部是 XMLConfigBuilder.parse() → 解析出 Configuration 对象 → 传给 DefaultSqlSessionFactory。Configuration 是全框架的配置中枢,所有东西最终都挂到它身上:
public class Configuration {
protected Environment environment; // 环境(数据源 + 事务工厂)
protected final Map<String, MappedStatement> mappedStatements; // ★ 每条 SQL 一条 MappedStatement
protected final Map<String, ResultMap> resultMaps; // 结果映射
protected final MapperRegistry mapperRegistry; // Mapper 接口注册表
protected final TypeAliasRegistry typeAliasRegistry; // 类型别名
protected final InterceptorChain interceptorChain; // 插件链
protected boolean cacheEnabled = true; // 二级缓存总开关
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE; // 执行器类型
...
}
2. MappedStatement:每条 SQL 的"编译产物"
解析 <select id="selectById" resultType="User"> 时,XMLStatementBuilder 生成一个 MappedStatement,key 是 namespace + "." + id(就是后面代理调用时的 statementId)。它保存了这条 SQL 的所有要素:
public final class MappedStatement {
private String id; // namespace.id
private SqlSource sqlSource; // ★ SQL 来源(动态/静态)
private SqlCommandType sqlCommandType; // SELECT/INSERT/UPDATE/DELETE
private List<ResultMap> resultMaps; // 结果映射
private Integer flushCacheRequired; // 是否清缓存
private boolean useCache; // 是否用二级缓存
...
}
3. #{} 和 ${} 的解析差异(SQL 注入的根源)
动态 SQL 由 XMLScriptBuilder 解析成 SqlSource:
#{}→StaticTextSqlNode+ParameterMapping:SQL 里被替换成?占位符,参数信息记录在ParameterMapping(property、typeHandler)里,运行时用PreparedStatement.setXxx()设值;${}→TextSqlNode:解析时直接字符串拼接进 SQL。
面试点:这就是防注入原理的答案——
#{}走预编译,参数永远只被当"值"处理,不会被当 SQL 片段解析;${}是拼接,参数里写' or 1=1 --就会改掉 SQL 语义。所以${}只用于动态表名/列名等"必须拼 SQL 片段"的场景,且要白名单校验。
三、会话层:SqlSession 与 Executor
SqlSession:门面
DefaultSqlSession 把 selectOne/selectList/insert/update/delete 全部委托给 Executor 执行,自身不碰 JDBC。注意:DefaultSqlSession 不是线程安全的(内部有一级缓存等状态),所以 Spring 整合时才有 SqlSessionTemplate 那套封装(第八节讲)。
Executor:执行器体系
Executor(接口)
├─ CachingExecutor(装饰器,二级缓存)
└─ BaseExecutor(一级缓存 localCache)
├─ SimpleExecutor 默认:每次执行都 new 一个 Statement
├─ ReuseExecutor 复用 Statement(同一 SQL 不重复预编译)
└─ BatchExecutor 批处理(连续 addBatch 后统一 executeBatch)
- 一级缓存:
BaseExecutor.query先查localCache(PerpetualCache),缓存 key =statementId + sql + 参数 + rowBounds + environmentId;作用域是 SqlSession,update操作会清空它; - 二级缓存:
CachingExecutor装饰,作用域是 namespace(mapper)级别,需cacheEnabled=true(默认)+ mapper 里配置<cache/>。
面试点:① 装饰器模式是 MyBatis 缓存设计的骨架(CachingExecutor 包 BaseExecutor);② 一级缓存只在同一个 SqlSession 内生效,Spring 整合后每个方法一个 SqlSession,一级缓存实际退化为"同一事务内有效";③ 二级缓存生产很少开——多表联查时,一个 namespace 的更新不会清另一个 namespace 的缓存,容易脏读。
四、Mapper 代理:接口为什么能调
getMapper 发生了什么
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
MapperRegistry.getMapper → MapperProxyFactory.newInstance:
public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),
new Class[] { mapperInterface }, mapperProxy); // ★ JDK 动态代理
}
方法调用时:MapperProxy.invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args); // toString/hashCode 直接执行
}
// 3.5.x:包装成 MapperMethodInvoker(默认方法/代理方法分开处理),核心是 MapperMethod
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
MapperMethod.execute 按 SQL 类型 × 返回类型 分派:
switch (command.getType()) {
case SELECT:
if (method.returnsVoid() && hasResultHandler()) { ... }
else if (method.returnsMany()) return executeForMany(sqlSession, args); // List
else if (method.returnsMap()) return executeForMap(sqlSession, args); // Map
else return sqlSession.selectOne(...); // 单对象
case INSERT / UPDATE / DELETE: ... rowCountResult(...)
}
参数处理由 ParamNameResolver 负责:单参数直接传(集合会特殊处理,如 foreach 场景);多参数包成 ParamMap,提供 arg0, arg1... 和 param1, param2... 两套 key,@Param("id") 可指定名字。
面试点:① 和 Feign、MyBatis-Plus 同一招——JDK 动态代理 + FactoryBean/工厂,可以串联答;② Mapper 接口方法不能重载——statementId 就是"接口全限定名.方法名",重载会冲突;③ 为什么多参数建议加 @Param——XML 里
#{arg0}这种名字可读性差,且编译保留参数名(-parameters)在有些场景不生效。
五、SQL 执行链路:一次查询怎么走完
mapper.selectById(1)
→ MapperProxy.invoke
→ MapperMethod.execute // 按类型分派
→ sqlSession.selectOne(statementId, 1) // 门面转发
→ CachingExecutor.query // ① 二级缓存(命中直接返回)
→ BaseExecutor.query // ② 一级缓存(命中直接返回)
→ SimpleExecutor.doQuery
→ Configuration.newStatementHandler
→ RoutingStatementHandler → PreparedStatementHandler
→ parameterize():ParameterHandler 用 TypeHandler set 参数
→ query():PreparedStatement.execute() // ★ 真正 JDBC 执行
→ DefaultResultSetHandler.handleResultSets
→ 自动映射(autoMapping)+ resultMap 映射
→ 反射创建 POJO + TypeHandler 类型转换
两个关键组件:
- StatementHandler:负责"语句"——
PreparedStatementHandler(默认,预编译)、StatementHandler、CallableStatementHandler(存储过程);RoutingStatementHandler 是路由门面; - ResultSetHandler:负责"结果"——按
ResultMap/自动映射,把 ResultSet 逐行反射成 POJO;自动映射规则是"列名(或驼峰转换后)与属性名相同",开启mapUnderscoreToCamelCase后user_name→userName。
面试点:延迟加载(懒加载)也发生在这里——
<association>配fetchType="lazy"时,先填充代理对象(CGLIB/Javassist),真正调 getter 时才用ResultLoader发第二条 SQL。所以 N+1 问题的根源就是"主查询每行再触发一次关联查询"。
六、插件机制:Interceptor
MyBatis 只允许拦截四大对象:Executor、StatementHandler、ParameterHandler、ResultSetHandler:
@Intercepts(@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class MyPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed(); // 环绕逻辑
}
}
注册后,Configuration.newExecutor/newStatementHandler 等地方调 Plugin.wrap 生成代理:
public static Object wrap(Object target, Interceptor interceptor) {
// 按 @Signature 匹配方法 → Proxy.newProxyInstance 层层包装
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), new Plugin(target, interceptor, signatureMap));
}
面试点:① 插件链不是"责任链",是多层动态代理嵌套(每注册一个插件多包一层,配置顺序决定执行顺序);② PageHelper 原理:拦截
Executor.query→ 解析原始 SQL 先select count(*)拿总数,再拼limit分页——这就是"拦截改写 SQL"的典型;③ 二级缓存、分页、脱敏都走这条扩展路,面试说"我写过 X 插件"时按 @Intercepts 四件套讲。
七、缓存总结(面试最爱问的三级联动)
| 缓存 | 作用域 | 开启方式 | 失效点 |
|---|---|---|---|
| 一级缓存 | SqlSession | 默认开启 | update 自动清空;SqlSession 关闭即销毁 |
| 二级缓存 | namespace(Mapper) | 总开关 + <cache/> |
该 namespace 的增删改清空它;跨 namespace 更新不会清 |
- 一级缓存脏读场景:两个 SqlSession,A 查完改库,B 用旧缓存读到脏数据;
- 二级缓存脏读场景:
UserMapper缓存了联查 Order 的结果,OrderMapper更新后 User 的缓存不知道; - 面试一句话:一级缓存是"会话内"的,二级缓存是"全局但按 mapper 隔离"的,整合 Spring 后一级缓存退化为事务内有效,二级缓存生产上基本靠 MyBatis-Plus 的一级缓存策略 + Redis 替代。
八、快速整合 SpringBoot 的原理(重头戏)
整合不是"把 MyBatis 放进 Spring",而是把 MyBatis 的每一个构建环节,替换成 Spring 容器的 Bean。原理链四步:
1. 自动装配入口:MybatisAutoConfiguration
mybatis-spring-boot-starter 通过 spring.factories 注册 MybatisAutoConfiguration(老朋友了,和 SpringBoot 执行流程那篇同款机制):
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class) // 有 DataSource 才生效
@EnableConfigurationProperties(MybatisProperties.class) // mybatis.* 前缀配置
public class MybatisAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); // ★ 又见 FactoryBean
factory.setDataSource(dataSource); // ① 数据源直接用 Spring 的
factory.setMapperLocations(resolveMapperLocations()); // ② 扫描 classpath*:mapper/**/*.xml
factory.setTypeAliasesPackage(...); // ③ 别名包
factory.setTransactionFactory(new SpringManagedTransactionFactory()); // ④ ★ 事务交给 Spring
return factory.getObject();
}
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory); // ★ 线程安全的 SqlSession 封装
}
}
整合的四个"替换"是答题核心:
- 数据源:不再写 mybatis-config 的
<environments>,直接注入 Spring 的 DataSource(连接池、监控都是 Spring 的); - 事务:
SpringManagedTransaction拿连接走DataSourceUtils.getConnection()——同一个@Transactional事务里,MyBatis 和 JdbcTemplate 拿的是同一条 Connection,这就是"整合后事务统一"的底层; - XML 扫描:
mybatis.mapper-locations配置(默认classpath*:mapper/**/*.xml); - Mapper 接口扫描:
@MapperScan或自动扫描(见第 3 步)。
2. Mapper 接口怎么变成 Bean:MapperFactoryBean
@MapperScan("com.xxx.mapper") → MapperScannerRegistrar → ClassPathMapperScanner 扫描包下所有接口,逐个注册成 MapperFactoryBean 的 BeanDefinition:
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface); // ★ 返回 JDK 动态代理
}
}
所以 @Autowired UserMapper 注入的正是 MapperFactoryBean.getObject() 的产物——和 FeignClientFactoryBean 一模一样的套路:接口无实现类,FactoryBean 生产代理。
不写 @MapperScan 也行:MybatisAutoConfiguration 里 @Import(AutoConfiguredMapperScannerRegistrar.class) 会默认扫描主类所在包(@AutoConfigurationPackage 标注的包)下带 @Mapper 注解的接口——这就是"主类加 @MapperScan 或接口加 @Mapper 二选一"的原理。
3. 线程安全怎么解决:SqlSessionTemplate
前面说过 DefaultSqlSession 非线程安全(持有一级缓存状态),而 Spring 的 Mapper Bean 是单例、被所有线程共享的。解法是 SqlSessionTemplate:
public class SqlSessionTemplate implements SqlSession {
// 内部 SqlSessionInterceptor 是 InvocationHandler → 又是一个动态代理
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = SqlSessionUtils.getSqlSession(sqlSessionFactory, ...);
try {
return method.invoke(sqlSession, args); // 转发给真正的 DefaultSqlSession
} finally {
SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
}
}
}
每个方法调用:从事务上下文拿(或新建)一个 SqlSession → 执行 → 用完即关。这样每个线程用的是自己的 SqlSession,单例 Bean 就线程安全了。
面试点:这也解释了"整合 Spring 后一级缓存为什么基本失效"——每个方法一个新 SqlSession,一级缓存(SqlSession 级)用完就没了;只有同一个事务内
SqlSessionUtils会复用同一个 SqlSession,一级缓存才生效。所以答一级缓存时一定要补这句"Spring 场景下的真实表现"。
4. 事务怎么无缝衔接:@Transactional 下的链路
@Transactional 方法
→ Spring 事务拦截器:开启事务,Connection 绑定到线程(TransactionSynchronizationManager)
→ Mapper 方法 → SqlSessionTemplate → SqlSessionUtils.getSqlSession
→ 当前线程已有 SqlSessionHolder?有则复用(同一个 SqlSession → 同一 Connection)
→ 无则新建并注册 SqlSessionSynchronization(事务结束时自动 closeSession)
→ MyBatis 拿连接:SpringManagedTransaction → DataSourceUtils.getConnection
→ 拿到事务绑定的那条 Connection ★ 关键
→ 事务提交/回滚 → 同步器回调关闭 SqlSession
一句话:MyBatis 不再管事务,只负责"从 Spring 手里借连接";事务的开启、提交、回滚全部由 Spring 事务管理器接管。
九、高频追问速答
Q1:MyBatis 和 JPA/Hibernate 的区别?
半自动 vs 全自动:MyBatis 自己写 SQL、结果手工/约定映射,灵活可控,适合复杂 SQL、报表、性能敏感场景;Hibernate 自动生成 SQL、管理对象状态,开发快但黑盒、复杂查询难优化。MyBatis-Plus 在 MyBatis 上做了"半自动的再包装"(通用 CRUD)。
**Q2:#{} 和 KaTeX parse error: Expected 'EOF', got '#' at position 12: {} 的区别?** `#̲{}` 预编译占位符(防注入、…{}` 字符串拼接(有注入风险,只用于动态表名/列名)。底层:一个是 ParameterMapping + setXxx,一个是 TextSqlNode 直接拼。
Q3:一级缓存和二级缓存?
一级 SqlSession 级默认开;二级 namespace 级需 <cache/>;整合 Spring 后一级缓存只在事务内有效;二级缓存有跨 namespace 脏读风险,生产少用。
Q4:MyBatis 怎么防 SQL 注入?#{} 预编译(PreparedStatement),参数与 SQL 结构分离;注入只能通过 ${} 拼接进入,所以 ${} 必须白名单校验。
Q5:Mapper 接口方法为什么不能重载?
statementId = 接口全限定名 + 方法名,重载会导致同一个 id 对应多条 SQL 定义,解析冲突。
Q6:延迟加载(N+1)原理?
关联属性先注入 CGLIB/Javassist 代理,getter 触发时用 ResultLoader 发第二条 SQL;一次主查询 N 行就 N 条关联查询 = N+1,解法是 join 或批量查询。
Q7:为什么 SqlSessionTemplate 是线程安全的?
它不是真 SqlSession,而是代理:每次方法调用从事务上下文拿/新建一个 DefaultSqlSession,用完关闭;每个线程操作自己的实例。
Q8:整合 SpringBoot 后,MyBatis 事务怎么被 Spring 接管的?SpringManagedTransactionFactory 让连接走 DataSourceUtils,事务内拿到的是 Spring 事务绑定的 Connection;SqlSession 复用由 SqlSessionUtils + 事务同步器保证。
十、总结:一条主线两个代理三个 FactoryBean
- 一条主线:
配置解析(Configuration + MappedStatement)→ getMapper 代理 → MapperMethod 分派 → Executor(缓存)→ StatementHandler → JDBC → ResultSetHandler 映射; - 两个代理:Mapper 接口的
MapperProxy、整合层的SqlSessionTemplate内部代理——MyBatis 的"巧劲"几乎全在动态代理上; - 三个 FactoryBean 式接入:
SqlSessionFactoryBean(生产 SqlSessionFactory)、MapperFactoryBean(生产 Mapper 代理)——和 Feign 的FeignClientFactoryBean同一套 Spring 装配思路,面试时可以三件套一起说,直接体现"框架模式"层面的理解; - 面试表达模板:“MyBatis 把 SQL 定义编译成 Configuration 里的 MappedStatement,运行时靠 JDK 代理把接口方法翻译成 statementId 的调用,执行交给 Executor/StatementHandler 两层;整合 SpringBoot 本质是把数据源、事务、扫描全部替换成 Spring 容器能力,再用 SqlSessionTemplate 解决线程安全。”
更多推荐



所有评论(0)