本文目录导读:

- 目录导读
- 开篇:为什么读MyBatis源码?
- 核心架构:从Configuration到SqlSession的初始化
- 源码案例一:Mapper接口代理是如何“凭空”生成的?
- 源码案例二:SQL语句解析与动态SQL的底层魔法
- 源码案例三:一级缓存与二级缓存的工作机制(含失效场景)
- 高频问答:解决你对MyBatis源码的终极困惑
- 源码学习路径与面试加分项
MyBatis源码深度剖析:从Mapper代理到SQL执行全链路(案例驱动)
目录导读
- 开篇:为什么读MyBatis源码?
- 核心架构:从Configuration到SqlSession的初始化
- 源码案例一:Mapper接口代理是如何“凭空”生成的?
- 源码案例二:SQL语句解析与动态SQL的底层魔法
- 源码案例三:一级缓存与二级缓存的工作机制(含失效场景)
- 高频问答:解决你对MyBatis源码的终极困惑
- 源码学习路径与面试加分项
开篇:为什么读MyBatis源码?
在日常开发中,MyBatis是ORM层的绝对主流,但大多数开发者停留在“用API”层面,一旦遇到“为什么Mapper接口没有实现类却能调用?”或“一级缓存为何在跨方法时失效?”这类问题,便无从下手,本文通过三个真实源码案例,带你从SqlSession构建、MapperProxy动态代理到BaseExecutor缓存策略,全链路拆解核心逻辑,这不仅能让你的排查问题能力提升一个维度,更是面试中“源码级”答案的底气。
核心架构:从Configuration到SqlSession的初始化
源码入口:SqlSessionFactoryBuilder.build() → XMLConfigBuilder解析全局配置文件 → 生成Configuration单例。
关键代码逻辑(简化):
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse()); // parse()返回Configuration
}
重点:Configuration是全局唯一对象,它持有MappedStatement(每个select/insert等的封装)、TypeAliasRegistry、MapperRegistry等。
源码案例一:Mapper接口代理是如何“凭空”生成的?
现象:你只写了UserMapper接口,定义了findById方法,没有写实现类,却直接userMapper.findById(1)。
源码破案:
- 调用
sqlSession.getMapper(UserMapper.class)。 - 进入
MapperRegistry.getMapper()→knownMappers中取出MapperProxyFactory。 MapperProxyFactory.newInstance()创建MapperProxy(实现了InvocationHandler)。MapperProxy.invoke()中,精准匹配方法名,将方法调用转换为MapperMethod执行。
关键代码:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
return mapperProxyFactory.newInstance(sqlSession);
}
MyBatis用JDK动态代理,将接口方法“翻译”成SQL执行请求,这就是你为何不用写实现类的原因。
源码案例二:SQL语句解析与动态SQL的底层魔法
场景:<if test="name != null"> 等动态标签如何被拼接成最终SQL?
源码流程:
- 解析阶段:
XMLStatementBuilder解析XML,将<if>、<where>等标签转换成SqlNode对象树。 - 执行阶段:调用
DynamicSqlSource.getBoundSql()→ 遍历SqlNode.apply()方法。
案例代码(IfSqlNode的简化逻辑):
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context); // 追加SQL片段
return true;
}
return false;
}
经验:动态SQL是“预编译节点树”,而非字符串拼接,这也是为什么能防注入,而不能——通过PreparedStatement参数占位符传递。
源码案例三:一级缓存与二级缓存的工作机制(含失效场景)
一级缓存(默认开启):位于BaseExecutor的localCache(PerpetualCache),生命周期与SqlSession绑定。
源码现象:
// 同一SqlSession,连续两次查询同SQL同参数
User u1 = session.selectOne("findById", 1);
User u2 = session.selectOne("findById", 1); // 直接返回缓存,不执行SQL
失效场景(源码如何触发):
- 执行
update/delete/insert时,clearLocalCache()被调用,清空一级缓存。 - 跨
SqlSession时,一级缓存天然无效。
二级缓存(需手动开启):存储在Mapper层面,跨SqlSession,本质上是CachingExecutor包装了BaseExecutor,先查二级缓存再查一级。
注意陷阱:二级缓存查询结果默认是浅拷贝,如果你返回同一个对象给多个线程,可能造成线程安全问题。
高频问答:解决你对MyBatis源码的终极困惑
Q1:MyBatis能直接执行存储过程吗?
A:可以,通过<select statementType="CALLABLE">,底层即CallableStatement,源码中StatementHandler会根据statementType选择不同的Statement实现。
Q2:为什么MyBatis的Mapper不能重载?
A:因为MapperMethod中使用了Method对象作为key,Java原生方法重载会导致签名不一致,无法唯一定位MappedStatement的id,源码MapperAnnotationBuilder会校验方法唯一性。
Q3:MyBatis的和在源码层面究竟有何不同?
A:被解析为ParameterMapping,最终用PreparedStatement.setXxx填充;直接通过TextSqlNode拼接字符串,不做预编译,源码中SqlSourceBuilder和DynamicSqlSource可以清晰看到区别。
Q4:MyBatis插件的原理是什么?
A:通过Interceptor拦截四大核心对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler),利用JDK代理,在这些对象的方法上执行pluginAll()。
源码学习路径与面试加分项
学习MyBatis源码,建议按“初始化 → 代理 → SQL解析 → 执行器 → 缓存 → 插件”的顺序,面试时,提到“MapperProxy动态代理”、“DynamicSqlSource组合模式”、“一级缓存生命周期与SqlSession绑定”等术语,远胜过“我会用”。
最后送你一个调试技巧:在MapperProxy.invoke()和BaseExecutor.query()打上断点,跟一遍实际数据流转,你会通透很多。
(本文基于MyBatis 3.5.x版本源码分析,结合社区实践,无外部引用链接。)