MyBatis源码案例

wen java案例 1

本文目录导读:

MyBatis源码案例

  1. 目录导读
  2. 开篇:为什么读MyBatis源码?
  3. 核心架构:从Configuration到SqlSession的初始化
  4. 源码案例一:Mapper接口代理是如何“凭空”生成的?
  5. 源码案例二:SQL语句解析与动态SQL的底层魔法
  6. 源码案例三:一级缓存与二级缓存的工作机制(含失效场景)
  7. 高频问答:解决你对MyBatis源码的终极困惑
  8. 源码学习路径与面试加分项

MyBatis源码深度剖析:从Mapper代理到SQL执行全链路(案例驱动)

目录导读

  1. 开篇:为什么读MyBatis源码?
  2. 核心架构:从Configuration到SqlSession的初始化
  3. 源码案例一:Mapper接口代理是如何“凭空”生成的?
  4. 源码案例二:SQL语句解析与动态SQL的底层魔法
  5. 源码案例三:一级缓存与二级缓存的工作机制(含失效场景)
  6. 高频问答:解决你对MyBatis源码的终极困惑
  7. 源码学习路径与面试加分项

开篇:为什么读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等的封装)、TypeAliasRegistryMapperRegistry等。


源码案例一:Mapper接口代理是如何“凭空”生成的?

现象:你只写了UserMapper接口,定义了findById方法,没有写实现类,却直接userMapper.findById(1)

源码破案

  1. 调用sqlSession.getMapper(UserMapper.class)
  2. 进入MapperRegistry.getMapper()knownMappers中取出MapperProxyFactory
  3. MapperProxyFactory.newInstance()创建MapperProxy(实现了InvocationHandler)。
  4. 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参数占位符传递。


源码案例三:一级缓存与二级缓存的工作机制(含失效场景)

一级缓存(默认开启):位于BaseExecutorlocalCachePerpetualCache),生命周期与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拼接字符串,不做预编译,源码中SqlSourceBuilderDynamicSqlSource可以清晰看到区别。

Q4:MyBatis插件的原理是什么? A:通过Interceptor拦截四大核心对象(ExecutorStatementHandlerParameterHandlerResultSetHandler),利用JDK代理,在这些对象的方法上执行pluginAll()


源码学习路径与面试加分项

学习MyBatis源码,建议按“初始化 → 代理 → SQL解析 → 执行器 → 缓存 → 插件”的顺序,面试时,提到“MapperProxy动态代理”、“DynamicSqlSource组合模式”、“一级缓存生命周期与SqlSession绑定”等术语,远胜过“我会用”。

最后送你一个调试技巧:在MapperProxy.invoke()BaseExecutor.query()打上断点,跟一遍实际数据流转,你会通透很多。


(本文基于MyBatis 3.5.x版本源码分析,结合社区实践,无外部引用链接。)

抱歉,评论功能暂时关闭!