Mybatis核心工作流程源码解读

2025-07-30 14:25
380
0

Mybatis源码因为相对简单比较适合想进行源码学习的初始材料,所以在此作为本源码系列的第一篇。本篇文章主要介绍Mybatis的核心工作流程,让初学者知道该从哪方面入手进行源码学习。

一、准备工作

Mybatis源码地址:GitHub | Gitee

当前使用的版本:Mybatis 3.5.13

示例工程Gitee地址:点击跳转

1、从Mybatis源码地址中把Mybatis源代码Clone下来,在Tags中找到3.5.13的版本并Checkout作为本文使用的版本。

2、下载示例工程,如下图所示把Mybatis的源文件改为我们Checkout到本地的源码路径。(注意要选择到src\main\java文件夹下)。

示例工程介绍:

整个示例工程的代码非常简单就不做过多介绍,在示例工程中有一个测试类TestSqlSession,是我们研究Mybatis源码的入口。代码如下:

二、Mybatis核心类和接口介绍

  • SqlSession接口:会话核心接口,作为MyBatis的主要工作接口,提供了执行SQL、获取Mapper和管理事务的核心方法
  • Configuration类:全局配置中心,作为MyBatis的全局配置类,包含了所有配置信息,所有组件都从这里获取配置
  • Executor接口:SQL执行器,负责执行SQL语句的核心接口
  • MappedStatement类:SQL语句映射类,封装了一个SQL语句的所有信息。
  • MapperProxy类:Mapper代理类,使用动态代理实现Mapper接口的代理类。其拦截Mapper接口方法的调用,将方法调用转换为SQL执行。
  • StatementHandler接口:语句处理器,负责处理JDBC Statement的创建和参数设置。
  • ResultSetHandler接口:结果集处理器,负责将ResultSet转换为Java对象。
  • ParameterHandler接口:参数处理器,将Java对象转换为JDBC参数,处理参数映射。

三、Mybatis加载流程源码解读

第一步,我们来看看Mybatis启动时都做了哪些操作?

通过如下两行我们可以看到,Mybatis的启动,其实就是加载全局配置文件丢给SqlSessionFactoryBuilder进行构件。

// 加载MyBatis配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 通过建造者模式创建复杂对象,Builder中会完成配置文件的加载和SqlSessionFactory的创建
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);

其中运用到了工厂模式和建造者模式:

  • 工厂模式是为了封装对象的创建细节,易于以后扩展,如果要改变SqlSessionFactory的创建方式,只需要修改工厂类即可,无需修改调用方的代码。
  • 建造者模式是为了封装复杂对象的构件过程,SqlSessionFactory的创建涉及到数据源、事务管理、映射文件等,通过SqlSessionFactoryBuilder可以把这复杂的构件过程进行封装,提高代码的可读性,并且根据不同的配置可以创建不同的SqlSessionFacotory实现。

TIP:Mybatis与Spring的级联(mybatis-spring)中也是采用类似的加载方式,读取Spring配置文件中的Mybatis相关配置,构件为Configuration实例,注入到SqlSessionFactoryBuilder中完成Mybatis的加载工作。

第二步:深入查看SqlSessionFactoryBuilder是如何构造SqlSessionFactory的

如下图所示,核心的代码就是如下3行:

  • 把全局配置文件的输入封装到Builder中
  • 再通过XMLConfigBuilder加载配置文件到Configuration对象中(包括全局配置文件和映射文件,映射文件中的CRUD标签存储在ConfigurationMap<String, MappedStatement> mappedStatements对象中,下文详解。)
  • 再把Configuration对象注入到DefaultSqlSessionFactory中生成SqlSessionFactory的实例

第三步:继续查看XMLConfigBuilder中的parse方法是如何解析配置文件到Configuration实例中

解析Mybatis配置的主方法如下图所示,对Mybatis的全局配置文件中所有节点都进行了解析。

再以解析properties节点为例,继续跟进,如下图所示,最终把节点中的所有数据都解析到configuration实例中。

第四步:映射文件的解析

上文第三步中,会调用mapperElement方法,此方法中就会解析mappers节点中配置的Mapper路径,找到映射文件放到XMLMapperBuilder中进行解析。

如下图所示,本文章的示例代码会执行下图方框中的内容,调用XMLMapperBuilder中的parse方法解析映射文件。

继续深入XMLMapperBuilder中的parse方法,找到解析Mapper XML的主要方法为configurationElement

以解析CURD相关元素(buildStatementFromContext)为例,继续往下查看代码,调用链如下。

中间调用链的代码在此就不一一展示了,直接跳到addMappedStatement函数中,看最终CURD的映射语句是如何加载的。

如上图所示,通过解析到的CRUD相关字段又会放到一个Builder中,通过Builder生成MappedStatement实例,最终放到Configuration实例的mappedStatement字段中。

要注意的一点,上图208行,存入Builder中的id是与命名空间拼接后生成的ID,而mappedStatement字段实际为一个Map,存入的key即为命名空间+标签ID。示例代码中通过SqlSession调用查询方法List<User> list = session.selectList("cyx.example.demo.mybatis.dao.UserMapper.selectAllUsers");传递的入参也为命名空间+标签ID。

四、SqlSession创建源码解读

通过上文可知,我们获得了DefaultSqlSessionFactory的实例,通过此实例调用openSession方法即可获得SqlSession。

SqlSession 接口也是外观模式(Facade Pattern)的典型应用,通过提供一个统一的接口,隐藏系统的复杂性,让客户端只需与高层接口交互。

// 获取SqlSession
SqlSession session = factory.openSession();

第一步,看看openSession方法中都干了啥

openSession方法的核心逻辑如下图所示。

在mybatis初始加载时已经把配置信息存入了configuration实例中,openSessionFromDataSource方法中其实就是把配置拿出来首先创建执行器,再把执行器注入到DefaultSqlSession中返回,即完成了SqlSession的创建。

第二步:看看执行器是如何创建的

如下图所示,执行器的创建也不复杂,根据执行类型ExecutorType生成不同的执行器,然后再进行缓存装饰和拦截器处理。

五、Mybatis查询流程源码解读

通过测试方法的第四行代码,我们深入追踪看看Mybatis的查询逻辑是怎样的:

List<User> list = session.selectList("cyx.example.demo.mybatis.dao.UserMapper.selectAllUsers");

第一步:查看selectList逻辑

从上文可知,SqlSession实例化的是DefaultSqlSession的实例,其selectList核心代码如下:

  • statement字段其实就是Mapper接口的类路径+方法名(也对应映射文件namespace+节点id)
  • 从Map中取出对应的MappedStatement(这即对应着映射文件中对应节点的数据),然后把这数据放到执行器中执行。

第二步:继续跟踪执行器是如何执行的

如下图,执行器是在上文SQLSession创建时生成的,可能分两种实现,CachingExecutor执行器就对应二级缓存的执行器。而BaseExecutor则是用的模板方法设计模式,其实现有默认的简单执行器、批处理执行器和重用执行器。

如果开启了二级缓存,则会进入CachingExecutor,我们首先来看下二级缓存是如何实现的

第三步:查看二级缓存实现逻辑

如下图所示,使用的也是很常规的缓存处理方式,如果缓存中有数据,则从缓存获取,没有则去查一遍数据库然后放到缓存中。

要注意的点:

  • 二级缓存和一级缓存创建缓存键使用的相同的创建逻辑,但通过不同的清空缓存的时机实现了不同的生命周期。
    • 一级缓存清空时机是会话级别。(SqlSession关闭时、执行commit、rollback、update时,或者手动调用clearLocalCache方法时)
    • 二级缓存清空时机是应用级别。(执行update时、事务提交时、手动调用cache.clear()时)
  • key的组成要素:SQL语句、参数值、分页参数、环境配置等。

第四步:查看BaseExecutor是如何执行查询

看起来代码这么多,其实核心逻辑还是判断localCache一级缓存中是否有数据,有就拿缓存中的数据,没有则执行查询逻辑。

第五步:继续追踪实际的查询数据库逻辑

上图中doQuery的逻辑在实际的执行器实现中,默认是SimpleExecutor,我们以SimpleExecutor为例继续往下看。

如上图所示,已经能看到JDBC的代码了,其中封装了语句处理器、参数处理器、结果集处理器,最后在语句处理器的query方法中进行数据库查询。

六、Mybatis是如何实现Mapper接口和映射文件的关联

上文中的测试方法都是使用的SqlSession中的查询方法直接调用查询,而实际项目中肯定不会写死statement字段,改为如下以动态代理的方式进行调用就会显得更加优雅。

        // 获取SqlSession
        SqlSession session = factory.openSession();
        // 一种方式是通过SqlSession提供的方法来操作数据库
        // List<User> list = session.selectList("cyx.example.demo.mybatis.dao.UserMapper.selectAllUsers");
        // 另一种方式,获取接口的动态代理对象直接调用
        UserMapper userMapper = session.getMapper(UserMapper.class);
        List<User> list = userMapper.selectAllUsers();

TIP:如果对动态代理不熟的可以查看我的另一篇文章:《Java动态代理源码分析》

第一步:查看getMapper方法的具体实现

直接往下追踪代码,可以看到如下图所示逻辑:

第二步:继续追踪MapperProxyFactory是如何newInstance的

从上图可以看出,最终的动态代理处理器是MapperProxy

第三步:查看MapperProxy的invoke方法

cachedInvoker方法主要是为了生成调用器,本示例中会生成普通方法调用器PlainMethodInvoker的实例。

继续查看普通方法调用器PlainMethodInvoker中的invoke方法

第四步:查看MapperMethod中的execute方法

如下图所示,很明显,最后动态代理中还是会根据CURD调用SqlSession中对应的方法执行数据库查询。

上文其实还有一些知识点没讲到,如:参数处理器、结果集处理器、拦截器,Mybatis与Spring的集成,后续有时间另开一篇文章再讲。

全部评论