深入MyBatis源码:初始化、执行链路与缓存机制全解析
做Java开发这么多年我始终觉得MyBatis源码分析是性价比极高的一项投入。很多人在项目里用了三五年MyBatis却说不清一条SQL从Mapper接口到数据库执行之间发生了什么面试被问到一级缓存、二级缓存时只会硬背结论。真正把源码打开读一遍之后你才能从“会用”变成“能懂”遇到诡异问题时才能自己定位。这篇文章不是源码逐行注释而是把我自己啃源码时最有价值的思考路径、核心环节和踩过的坑整理出来希望对准备深入MyBatis的人有帮助。1. 为什么值得花时间啃一遍MyBatis源码1.1 MyBatis在Java持久层中的地位与源码价值MyBatis在国内Java后端项目中的占有率极高尤其是传统业务系统和中小团队几乎没有哪个项目能绕开它。但多数人的使用方式停留在“写接口、写XML、注入Mapper”的套路里对底层原理缺乏系统认知。源码分析的意义在于它能帮你回答一系列实际工作中高频出现的问题为什么Mapper接口没有实现类却能执行SQL为什么一级缓存有时候不生效二级缓存为什么会出现脏数据为什么if条件明明写了却不生效这些问题不看源码只能靠猜。MyBatis源码规模在主流ORM框架中属于中等偏小核心包只有几十个类没有Spring那种庞大的依赖体系非常适合作为第一个通读源码的框架。它把JDBC的样板代码全部封装进SqlSession、Executor、StatementHandler这些组件中保留了SQL的灵活控制权。读懂MyBatis源码你不仅能理解框架本身的执行模型还能顺带熟悉JDBC、动态代理、装饰器模式、反射、泛型等Java基础技能的实战用法一箭多雕。1.2 源码阅读前的三个核心认知接口、代理与SqlSession读源码前需要建立三个底层认知否则容易迷失在类与类的关系网里。第一Mapper接口本身没有实现类你调用userMapper.selectById(1)实际是通过JDK动态代理生成的一个代理对象在干活。这个代理对象由MapperProxyFactory创建所有接口方法调用最终都会转发到MapperProxy.invoke再由它决定执行哪条MappedStatement、使用哪个SqlSession。第二SqlSession是MyBatis对外的门面但它自己不干活真正干活的是Executor。SqlSession更像一个调度中心你调selectList它就找到对应的MappedStatement然后交给Executor.query去执行。搞清楚这个关系后很多疑惑会迎刃而解比如缓存逻辑并没有写在SqlSession里而是写在Executor层。第三XML配置和注解最后都会汇聚到一个统一的模型上——Configuration。它是全局配置的容器几乎所有解析结果都塞进这个类里包括MappedStatement、ResultMap、TypeHandler、MapperRegistry等。理解了Configuration你就理解了MyBatis的“心脏”。1.3 我推荐的源码阅读路线从使用到原理自上而下我不建议一上来就从XMLConfigBuilder逐行读那样很容易被XML标签解析细节劝退。我自己的路线是先跑一个最简单的MyBatis demo打上断点从SqlSessionFactoryBuilder.build()开始跟。跟完初始化流程后再看一次完整的select执行链路明白SqlSession - Executor - StatementHandler - ResultSetHandler这条主干。回到初始化源码重点读XMLConfigBuilder和XMLMapperBuilder把“XML标签变成配置对象”的过程补齐。最后单独研究缓存和TypeHandler这两个高频面试点。这样可以先建立整体骨架再填充细节不会一上来就被细枝末节淹没。源码阅读切忌贪多求快每读一个类都要问自己“这个类解决了什么问题”而不是“这个方法是怎么实现的”。带着问题读效率会高很多。2. 初始化阶段源码拆解从XMLConfigBuilder到Configuration2.1 XMLConfigBuilder是如何一步步解析mybatis-config.xml的几乎所有使用XML配置的MyBatis项目入口都是SqlSessionFactoryBuilder.build(InputStream)。方法内部会先创建XMLConfigBuilder然后调用parse()返回Configuration。XMLConfigBuilder最核心的方法就是parse()和parseConfiguration(XNode root)。parseConfiguration的代码非常有规律几乎是按XML根节点的子标签顺序逐个解析properties、settings、typeAliases、typeHandlers、objectFactory、objectWrapperFactory、reflectorFactory、plugins、environments、databaseIdProvider、mappers。读这段源码时你不需要记住每个方法只需要掌握一个规律每个xxx节点对应一个configurationElement()方法方法内部从XNode取出属性或子节点再注入到Configuration对象里。这里有个我当年忽略的细节XMLConfigBuilder在构造时会调用XPathParser.newParser()创建XPathParser并基于JDK的XPath解析XML。所以即使你不懂XML解析原理也能在这里复习一遍org.w3c.dom和javax.xml.xpath的用法。XNode是MyBatis对XML元素的封装提供了getStringAttribute、getBooleanAttribute、evalNode、evalNodes等方法后续所有标签解析都用它来读取数据。2.2 Mapper映射文件的解析XMLMapperBuilder与MappedStatementMyBatis配置文件中mappers标签的作用是告诉框架去哪儿找Mapper映射文件。最常见的写法是mapper resourcemapper/UserMapper.xml/而对应的解析流程在XMLConfigBuilder.mapperElement方法里。它会根据子节点类型分别处理package扫描包、resource加载文件路径、url加载远程或本地URL、mapperClass注册接口类。无论哪种方式最终都会调用configuration.addMapper()进入MapperRegistry。对于XML文件XMLMapperBuilder会解析出namespace、resultMap、sql片段、select/insert/update/delete等节点。每个SQL节点最终会被构造成一个MappedStatement对象保存SQL语句、参数映射、结果映射、statement类型等。这是MyBatis初始化阶段最核心的产出MappedStatement相当于一条SQL的“元数据包”后续执行阶段Executor要用它来创建StatementHandler。我在读这段源码时有个特别深的感受MyBatis对“映射”的抽象非常统一。无论你是用XML写select还是用注解Select最后都会回到MappedStatement。注解方式只是通过MapperAnnotationBuilder把注解内容解析成同样的模型罢了。这种“统一模型”的设计思路值得我们在自己项目里借鉴多个配置来源最终归一化到同一个内部表示。2.3 MapperRegistry与MapperProxyFactory接口是怎么变成代理对象的MapperRegistry是Configuration中负责管理Mapper接口注册表的组件。它维护了一个knownMappers集合里面存放Class?和MapperProxyFactory?的映射。调用configuration.addMapper(UserMapper.class)时会往里面塞一个MapperProxyFactory。但注意这一步只是“注册”并不会立刻创建代理对象。真正的代理对象生成是在MapperRegistry.getMapper(ClassT type, SqlSession sqlSession)方法中。它会从knownMappers中取出对应的MapperProxyFactory再调用factory.newInstance(sqlSession)。MapperProxyFactory.newInstance核心逻辑如下public T newInstance(SqlSession sqlSession) { MapperProxyT mapperProxy new MapperProxyT(sqlSession, mapperInterface, methodCache); return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }也就是说每次从Spring容器里取一个Mapper其实就是通过这个工厂生成了一个JDK动态代理对象。MapperProxy.invoke方法会判断方法是不是Object的方法如toString、hashCode是则放行否则从methodCache中查找对应的MapperMethod然后调用MapperMethod.execute来执行SQL。读到这里你会发现MyBatis把“接口”和“SQL映射”解耦得异常干净这也是它比传统DAO实现类更灵活的原因。2.4 TypeHandler的注册流程与自定义实现TypeHandler是MyBatis中容易被人忽略又很重要的组件。它负责Java类型和JDBC类型之间的双向转换写参数时把Java对象转成PreparedStatement可用的setObject参数读结果集时把ResultSet的值转成Java对象。默认的TypeHandler有几十个注册逻辑在TypeHandlerRegistry中完成。注册流程分几个入口一是XMLConfigBuilder解析typeHandlers标签二是MyBatis启动时自动注册内置TypeHandler三是通过TypeHandler注解或TypeHandler类扫描注册。每个TypeHandler都会注册到MapJdbcType, TypeHandler?或MapClass?, TypeHandler?中。当MyBatis执行参数映射时会根据Java类型、JdbcType以及typeHandler属性去注册表里“按图索骥”。自定义TypeHandler的实战场景很多比如枚举转换、JSON字段处理、加密字段处理。实现方式就是继承BaseTypeHandlerT实现setNonNullParameter、getNullableResult、getNullableResult等四个方法然后注册到configuration。源码虽然简单但它能帮你理解为什么自定义TypeHandler能生效以及为什么需要关注MappedJdbcTypes和MappedTypes注解——它们决定了MyBatis在何时能找到你的处理器。3. 核心执行链路源码解析3.1 SqlSession到Executor一次数据库操作的第一站执行链路从SqlSession开始。以selectOne为例它会调用selectList然后取列表第一个元素。DefaultSqlSession.selectList的逻辑是public E ListE selectList(String statement, Object parameter, RowBounds rowBounds) { MappedStatement ms configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); }这里的executor来自SqlSession构造时传入的Executor实例。如果你没有额外配置默认是SimpleExecutor如果开启了二级缓存就是CachingExecutor它会包一层装饰器。Executor内部维护了Transaction通过transaction.getConnection()获取JDBC Connection。一级缓存相关的逻辑也在Executor层具体是BaseExecutor.query里的queryFromDatabase流程。很多人以为SqlSession关闭了连接才会释放其实MyBatis的SqlSession本身不持有连接管理逻辑事务和连接生命周期都由Executor和Transaction管理。这也是为什么Spring整合后连接是委托Spring的事务管理器来控制的。读源码时扣住这个点后面理解Spring整合就不再迷茫。3.2 StatementHandler、ParameterHandler、ResultSetHandler三兄弟的分工Executor拿到MappedStatement后会创建StatementHandler。这里我强烈建议重点读PreparedStatementHandler因为绝大多数SQL都是用预编译方式执行的。StatementHandler负责四件事prepare创建Statement并设置超时、fetchSize等。parameterize调用ParameterHandler设置预编译参数。query/update执行SQL并返回结果。parameterize和query之间通过ResultSetHandler处理结果集。ParameterHandler只有一个实现类DefaultParameterHandler它的核心逻辑是遍历MappedStatement的ParameterMapping逐个调用TypeHandler.setParameter把Java参数写入PreparedStatement。这里你就会发现TypeHandler真的是贯穿执行链路的。ResultSetHandler的核心是DefaultResultSetHandler它会根据ResultMap构建对象、自动映射列名、处理一对多和嵌套查询。三兄弟的分工清晰也体现了MyBatis的“单一职责”。如果你想扩展自定义逻辑比如SQL重写、结果加密解密可以在Configuration中设置自定义的StatementHandler、ParameterHandler或ResultSetHandler但一般情况下建议用Interceptor插件机制更安全也更灵活。3.3 一级缓存与二级缓存源码实现装饰器模式与TransactionalCache缓存是MyBatis源码分析中的重头戏也是面试必考。一级缓存是SqlSession级别的默认开启且无法关闭只能调到statement级别。它的实现在BaseExecutor中用PerpetualCache维护一个MapCacheKey, Object每次query之前先看缓存里有没有。缓存的key由CacheKey对象构造包含MappedStatement的ID、SQL语句、参数、RowBounds等。注意一级缓存作用范围是同一个SqlSession如果是Spring整合后的每个Mapper操作使用不同SqlSession或每次SqlSession用完就关闭一级缓存基本等于没怎么用到。二级缓存是namespace级别的跨SqlSession共享。它的实现使用装饰器模式CachingExecutor内部持有一个TransactionalCacheManager负责管理多个TransactionalCache。TransactionalCache包装了真正的Cache实现如PerpetualCache、LRUCache、ScheduledCache等同时维护了一个EntriesToAddOnCommit集合。查询时先从二级缓存取取不到再走一级缓存和数据库提交事务时才把暂存的数据刷新到真正的缓存中。这个设计能避免在事务未提交时读到脏数据很巧妙。读缓存源码时我特别建议多关注CacheKey的构建规则。很多所谓“缓存不生效”的问题本质上就是CacheKey不同比如多了一个空格、参数类型不同、分页参数变化都可能导致缓存命中失败。理解了CacheKey你就能解释为什么MyBatis官方不建议在有多表关联的查询上使用二级缓存——一旦其他namespace更新了数据当前namespace的缓存无法感知容易读到旧数据。4. 与Spring Boot整合中的源码视角4.1 MyBatis-Spring如何接管SqlSessionFactory在Spring Boot项目里你通常只是添加mybatis-spring-boot-starter依赖复杂的初始化过程就被自动配置类接管了。从源码角度看MybatisAutoConfiguration会做这几件事创建SqlSessionFactory、注册SqlSessionTemplate、注册Mapper扫描器。SqlSessionFactoryBean实现了FactoryBeanSqlSessionFactory初始化时会调用buildSqlSessionFactory()这一步和原生MyBatis的解析流程大体一致但多了Spring相关的资源加载和属性注入。SqlSessionTemplate是MyBatis-Spring中最重要的类它实现了SqlSession接口将方法调用委托给内部的SqlSessionProxy。SqlSessionProxy通过JDK动态代理在每次操作前从Spring事务管理器中获取SqlSession并绑定到当前线程操作结束后按事务状态决定提交或回滚。这就是为什么在Spring里你不需要手动关闭SqlSession——它由Spring容器统一管理。理解了这一层你会明白Spring整合后一级缓存为什么经常不生效。因为SqlSessionTemplate每次操作都可能是新的SqlSession除非你在同一个事务里MyBatis-Spring才会让你获取同一个SqlSession。这也解释了为什么Spring环境下想要利用一级缓存需要保证在同一个事务内多次查询。4.2 常见问题排查SQL条件不生效、日志不打印SQL、批量插入性能从源码角度回看几个高频问题会有豁然开朗的感觉。SQL条件不生效最常见的原因是if testparam ! null中的OGNL表达式写错或者参数绑定方式不对。比如传入的map里没有对应键表达式直接解析为false这样MyBatis在动态SQL拼接时就会跳过这个片段。排查时可以打开logImpl配置把SQL打印出来看最终生成的SQL是不是少了条件而不是怀疑数据库。日志不打印SQL一般和settings里logImpl配置有关。MyBatis支持SLF4J、LOG4J2、STDOUT_LOGGING等如果你配置了setting namelogImpl valueSLF4J/那么需要保证日志框架的level在DEBUG或TRACE。另外默认的SimpleExecutor会在DEBUG级别打印SQL、参数和返回行数如果看不到检查你的日志配置是否覆盖了Mapper接口所在包。批量插入性能问题根源在于Executor类型没选对。MyBatis默认SimpleExecutor会为每个SQL语句创建单独的PreparedStatement循环插入N条就是N次prepare、N次execute性能很差。改成ExecutorType.BATCH或者使用MyBatis的SqlSessionTemplate的batch模式就能复用同一个PreparedStatement。源码中BatchExecutor.doUpdate会维护一个Statement列表批量提交时一次性执行理解这个机制后你就知道为什么需要定期flush或提交。4.3 MyBatis-Plus与原生MyBatis的源码关系很多项目使用MyBatis-Plus它并不是MyBatis的替代品而是以MyBatis插件和基础增强的形式存在。MyBatis-Plus的核心是在MybatisConfiguration中新增了GlobalConfig通过MybatisMapperRegistry和自定义的MybatisMapperAnnotationBuilder来增强Mapper。它提供了BaseMapper中的通用方法这些方法并不需要你写XML而是通过AbstractMethod拼装SQL最终也是生成MappedStatement。从MyBatis源码视角看MyBatis-Plus做的事情其实很朴素利用MyBatis的插件机制拦截Executor和StatementHandler或者注册额外的MappedStatement从而实现分页、自动填充、逻辑删除、乐观锁等功能。理解了原生MyBatis的Configuration和MappedStatement模型再去看MyBatis-Plus的源码会轻松很多因为它的核心扩展点都在Configuration的继承和Interceptor的拦截逻辑上。5. 面试与实战中的高频考点复盘5.1 核心类关系一张表理清Configuration、XMLConfigBuilder、MappedStatement面试时对方常会问“MyBatis初始化过程是怎样的”如果你能现场画出类关系并说清职责基本就过关了。我习惯用表格来梳理类名职责核心方法SqlSessionFactoryBuilder构建入口创建XMLConfigBuilderbuild()XMLConfigBuilder解析mybatis-config.xml维护XPathParserparse()、parseConfiguration()XMLMapperBuilder解析Mapper XML文件构建MappedStatementconfigurationElement()、buildStatementFrom()Configuration全局配置容器存放所有解析结果addMapper()、getMappedStatement()MapperRegistry管理Mapper接口与MapperProxyFactoryaddMapper()、getMapper()MapperProxyFactory创建Mapper接口的JDK代理newInstance()MappedStatement一条SQL的元数据模型getBoundSql()、getStatementType()面试回答时先把SqlSessionFactoryBuilder如何通过XMLConfigBuilder生成Configuration讲清楚再讲Configuration如何通过XMLMapperBuilder解析Mapper文件生成MappedStatement最后讲MapperRegistry如何注册接口。这套流程串下来面试官就会确信你是真的读过源码而不是背面试题。5.2 缓存面试题源码级回答一级缓存与二级缓存如何协作关于缓存面试中经常连环问一级缓存在哪里基于什么实现作用范围是什么二级缓存如何开启数据存放在哪里为什么可以使用装饰器模式一级缓存什么时候被清空两个不同SqlSession查询同一个数据二级缓存能命中吗多表查询能用二级缓存吗用源码去回答这些问题会特别有说服力。一级缓存在BaseExecutor的localCache字段中update操作会调用clearLocalCache()清空一级缓存二级缓存在CachingExecutor中通过TransactionalCacheManager管理默认是PerpetualCache。二级缓存的作用域是namespace所以多表关联查询时如果关联表的namespace更新了当前namespace的二级缓存不会被置空就会出现脏数据。这也是为什么官方建议二级缓存只用于单表操作、且对实时性要求不高的场景。5.3 一次自定义TypeHandler实战枚举与JSON字段处理实战中我经常用TypeHandler处理两类问题枚举转int、JSON字符串转List。以MySQL存储JSON字段为例我会写一个JsonTypeHandler extends BaseTypeHandlerListMyPojo重写四个方法。setNonNullParameter里把List序列化成JSON字符串getNullableResult里用Jackson反序列化。注册方式可以放在mybatis-config.xml的typeHandlers标签下也可以直接在字段的TableField(typeHandler JsonTypeHandler.class)注解上指定。这次实战让我明白了TypeHandler注册机制的价值MyBatis中同一个Java类型可能有多个TypeHandler通过MappedTypes和MappedJdbcTypes组合条件来区分。如果你的自定义TypeHandler不能生效多半是缺少这两个注解或者没有在配置中显式声明导致MyBatis在自动注册时无法匹配。源码中TypeHandlerRegistry.getTypeHandler的查找逻辑一步步排除看一遍就再也不会犯这种错。最后分享一点读源码的体会读MyBatis源码这件事我最大的收获不是记住了多少类名而是学会了“顺着调用链去理解框架”的方法。拿到任何一个新框架先找入口再追主流程再回到配置解析最后看扩展机制这条思路可以用在Spring、Netty、Dubbo上。MyBatis的源码风格简洁命名清晰适合作为源码入门的第一个项目。如果你正在被一堆类绕晕建议只盯着一条查询链路反复打断点从MapperProxy.invoke一路走到ResultSetHandler的handleResultSets跑通这条线后再回头看不理解的部分。几个晚上下来你会发现那些曾觉得神奇的“自动映射”和“缓存机制”原理其实比想象中简单得多。