MyBatis源码讲解

这一节会对 MyBatis 源码做一个概括性介绍,为大家阅读源码提供一个帮助。在讲解源码前,先按照上一节中的操作获取源码,可以先 Fork 官方仓库,然后再检出到本地,也可以直接从官方仓库检出到本地。

使用 Git 检出方式获取代码的好处是,可以看到详细的日志更新信息,配合一些 IDE 的插件还可以很方便地对文件进行比对,并且当官方源码有更新时,可以很方便地通过 Git 和官方仓库进行同步。使用 Git 的标签功能还可以切换当前代码为 MyBatis 的不同版本,在检出的 MyBatis 项目中,打开 Git Bash,输入如下命令。

$ git tag
...

执行上面的命令,输出结果省略了部分标签。假如当前使用的 MyBatis 为 3.2.8 版本,想要查看 3.2.8 版本的源码可以通过以下命令实现。

$ git checkout mybatis-3.2.8
...

这时源码的版本就变成了 3.2.8 版本。若要换回最新版本的代码,只需要输入以下命令切换到 master 分支即可。

$ git checkout master
...

如果并不需要 Git 提供的这些便利,最简单的下载源码的方式就是从官方仓库直接下载 zip 包,在浏览器中打开 https://github.com/mybatis/mybatis-3 ,点击右侧的 Clone or download 按钮,弹出下拉菜单,如图11-13所示。

image 2024 05 25 13 19 46 778
Figure 1. 图11-13 克隆或下载

点击 Download ZIP 按钮即可开始下载,由于这种方式并不包含版本信息,因此下载的文件并不大,一般都能很快下载完成。

MyBatis 官方的项目基本都是使用 Maven 管理依赖的,所有的项目依赖都继承自 parent 项目。例如,在我们下载的 MyBatis 源码的 pom.xml 中就有如下的 parent 配置。

<parent>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-parent</artifactId>
    <version>28</version>
    <relativePath />
</parent>

由于从官方获取的源码版本比较新,源码的版本号很可能没有打包上传到 Maven 的官方仓库,因此为了能够正常地编译 MyBatis,我们把 parent 项目也克隆(下载)到了本地,parent 项目地址为 https://github.com/mybatis/parent ,检查 parent 项目的版本号是否符合要求。

如果是克隆到本地的,版本不一致时可以用之前的命令查看项目的标签,然后切换到对应的版本。如果是使用下载方式下载到本地的,也可以手动修改为相同的版本号,或者采用更安全的方式从 GitHub 中选择对应的版本号,然后再下载。切换版本号的方式如图11-14所示。

image 2024 05 25 13 22 16 666
Figure 2. 图11-14 切换分支

点击左侧的 Branch 按钮,然后点击 Tags 标签,从列表中选择需要的版本。切换到相应的版本后,再点击右侧的 Clone or download 进行下载即可。

下载 parent 项目后,可以在 parent 根目录下通过执行 mvn install 命令将 parent 项目安装到本地仓库,也可以将其导入到 IDE 后通过 IDE 进行打包安装。

下载好源码后,将源码以 Maven 方式导入到 Eclipse 中,导入后的项目如图11-15所示。

到这里,查看源码的准备工作就完成了。

image 2024 05 25 13 23 21 617
Figure 3. 图11-15 MyBatis源码结构

在讲解源码前,我们通过一段示例代码将 MyBatis 中的各部分代码串联起来了,通过这段代码可以了解 MyBatis 各部分的作用。这一节中的代码会以本书第 1 章中创建的 simple 项目为基础(后面章节的内容对本节示例没有影响),并在此环境中编写示例代码。

为了更全面地进行演示,示例代码中会用到拦截器。先来编写一个最简单的拦截器(详细内容请参考第 8 章),代码如下。

这个拦截器很简单,只是在方法执行前后输出了一段文件,通过 invocation.proceed() 直接执行被拦截的方法。

示例代码位于 src/main/test 目录下,在 tk.mybatis.simple 包下,测试类为 SimpleTest,整体结构如下。

所有即将用到的 import 类几乎都涵盖了重要的包。下面分段编写上述代码中注释位置的代码。

第一部分,指定日志和创建配置对象。

默认情况下,MyBatis 会按照默认的顺序去寻找日志实现类,日志的优先级顺序为SLF4j>Apache Commons Logging>Log4j2>Log4j>JDK Logging>StdOut Logging>NO Logging,只要 MyBatis 找到了对应的依赖,就会停止继续找。因此,如果想要指定某种日志实现,可以手动在 LogFactory 初次初始化前调用指定的方法,上述代码通过调用 useLog4JLogging 方法指定使用 Log4j,这个方法生效的前提是,项目中必须有 Log4j 的依赖,否则将不会生效。

后续创建的 Configuration 对象是 MyBatis 中最重要的一个类,这个类几乎包含了 MyBatis 全部的内容,记录了 MyBatis 的各项属性配置。常见的 settings 中的配置基本都可以通过 Configuration 来完成。

第二部分,添加拦截器。

这里按照顺序依次将拦截器 1 和 2 添加到 config 中,在后面的执行过程中可以看到这两个拦截器的执行顺序。

第三部分,创建数据源和 JDBC 事务。

使用 MyBatis 提供的最简单的 UnpooledDataSource 创建数据源,使用 JDBC 事务。

第四部分,创建 Executor。

final Executor executor=config.newExecutor(transaction);

Executor 是 MyBatis 底层执行数据库操作的直接对象,大多数 MyBatis 方便调用的方式都是对该对象的封装。通过 Configuration 的 newExecutor 方法创建的 Executor 会自动根据配置的拦截器对 Executor 进行多层代理。通过这种代理机制使得 MyBatis 的扩展更方便,更强大。

第五部分,创建SqlSource对象。

StaticSqlSource sqlSource = new StaticSqlSource(config, "SELECT * FROM country WHERE id = ?");

无论通过 XML 方式还是注解方式配置 SQL 语句,在 MyBatis 中,SQL 语句都会被封装成 SqlSource 对象。XML 中配置的静态 SQL 会对应生成 StaticSqlSource,带有 if 标签或者 ${} 用法的 SQL 会按动态 SQL 被处理为 DynamicSqlSource。使用 Provider 类注解标记的方法会生成 ProviderSqlSource。所有类型的 SqlSource 在最终执行前,都会被处理成 StaticSqlSource。

第六部分,创建参数映射配置。

在第五部分的 SQL 中包含一个参数 id。MyBatis 文档中建议在 XML 中不去配置 parameterMap 属性,因为 MyBatis 会自动根据参数去判断和生成这个配置。在底层中,这个配置是必须存在的。

第七部分,创建结果映射。

这种配置方式和在 XML 中配置 resultMap 元素是相同的,经常使用 resultType 方式,在底层仍然是 ResultMap 对象,但是创建起来更容易。

ResultMap resultMap = new ResultMap.Builder(
        config,
        "defaultResultMap",
        Country.class,
        new ArrayList<ResultMapping>()
).build();

上面的 Builder 中的最后一个参数为空数组,MyBatis 完全通过第 3 个参数类型来映射结果。

第八部分,创建缓存对象。

这是 MyBatis 根据装饰模式创建的缓存对象,通过层层装饰使得简单的缓存拥有了可配置的复杂功能。各级装饰缓存的含义可以参考上述代码中对应的注释。

第九部分,创建 MappedStatement 对象。

上述第一、五、六、七、八五个部分已经准备好了一个 SQL 执行和映射的基本配置,MappedStatement 就是对 SQL 更高层次的一个封装,这个对象包含了执行 SQL 所需的各种配置信息。创建 MappedStatement 对象的代码如下。

MappedStatement.Builder 的第二个参数就是这个 SQL 语句的唯一 id,在 XML 和接口模式下就是 namespace 和 id 的组合。

现在有了 SQL 封装的 MappedStatement 对象和执行 SQL 的 Executor 对象,下面就可以执行 MappedStatement 中定义的这个查询了,查询代码如下。

List<Country> countries = execcutor.query(
        ms, 1L, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER
);

该方法的第 1 个参数为 MappedStatement 对象,第 2 个参数是方法执行的参数,这里就是第六部分创建的参数映射对应的参数值。第 3 个参数是 MyBatis 内存分页的参数,默认情况下使用 RowBounds.DEFAULT,这种情况会获取全部数据,不会执行分页操作。第 4 个参数在大多数情况下都是 null,用于处理结果,因为 MyBatis 本身对结果映射已经做得非常好了,所以这里设置为 null 时可以使用默认的结果映射处理器。

执行上面的方法便可以得到查询的结果。当使用 MyBatis 时,项目启动后就已经准备好了所有方法对应的 MappedStatement 对象。在执行 MyBatis 的数据库操作时,底层就是通过调用 Executor 相应的方法来执行的。

完成以上几个部分,MyBatis 的主要执行过程便大致完成了。

上面的代码使用 Executor 执行并不方便,因此可以再提高一个层次,将上面的操作封装起来,使其更方便调用,代码如下。

config.addMappedStatement(ms);
SqlSession sqlSession = new DefaultSqlSession(config, executor, false);

首先将 MappedStatement 添加到 Configuration 中,在 Configuration 中会以 Map 的形式记录,其中 Map 的 key 就是 MappedStatement 的 id,这样就可以很方便地通过 id 从 Configuration 中获取 MappedStatement 了。在使用完整 id 保存的同时,还会尝试使用 “.” 分割最后的字符串(通常是方法名)作为 key 并保存一份。如果 key 已经存在,就会标记该 key 有歧义,这种情况下若通过短的 key 调用就会因为有歧义而抛出异常。

然后再将 Configuration 和 Executor 封装到 DefaultSqlSession 中,有了这两项就能方便地通过 MappedStatement 的 id 来调用相应的方法了,代码如下。

Country country=sqlSession.selectOne("selectCountry",2L);

注意,这里的 selectCountry 不存在歧义,也没有重复,所以可以直接使用短的名字进行调用,使用完整 id 也可以。

Country country = sqlSession.selectOne("tk.mybatis.simple.SimpleMapper.selectCountry", 2L);

到这一步,从 iBATIS 转到 MyBatis 的人应该最熟悉不过了,MyBatis 早期都是通过这种方式进行调用的,后来就有人将这种调用封装为 DAO 接口和 DAOImpl 实现类,在实现类中通过上面的方式进行调用。

这种用法非常麻烦,当有任何一点改动时,都需要同时修改好几处代码,并且由于 DAOImpl 中的代码形式一致,实现这种方法完全成了一种单纯消耗劳动力的工作。

再后来,MyBatis 使用 JDK 动态代理解决了 DAO 接口实现类的问题,使得我们使用起来更加方便。还可以再提高一个层次,使用动态代理的方式实现接口调用。先在 tk.mybatis.simple 包下创建 SimpleMapper 接口,代码如下。

package tk.mybatis.simple;

import tk.mybatis.simple.model.Country;

public interface SimpleMapper {
    Country selectCountry(Long id);
}

接口中的包名、接口名以及接口中的方法名、参数、返回值类型都是由上面各个部分的代码共同决定的,接口和 XML 混合模式实际上要求接口要和 XML 保持一致。

有了接口后,使用如下代码创建代理对象。

MapperProxyFactory<SimpleMapper> mapperProxyFactory = new MapperProxyFactory = new MapperProxyFactory<SimpleMapper>(SimpleMapper.class);
// 创建代理接口
SimpleMapper simpleMapper = mapperProxyFactory.newInstance(sqlSession);
// 执行方法
Country country = simpleMapper.selectCountry(3L);

动态代理工厂类创建动态接口时传入了参数 SqlSession,这是对 SqlSession 更高层次的封装,从上面的方法调用也能看出使用接口是多么方便。

MyBatis 底层的层层封装包含了很多细节,在使用时并不需要了解封装的具体过程,只要能够直接利用接口方式去执行各种各样的方法即可。

阅读 MyBatis 的源码可以学到很多东西,如缓存的装饰模式、大量 Builder 类的建造者模式、拦截器的代理链调用等。这一节的内容虽然尽可能涵盖 MyBatis 的方方面面,但是仍然有很多细节隐藏在各个步骤的实现类中没有单独说明,比如 MyBatis 将 ResultSet 映射到对象中的方法,再比如处理复杂的关联查询以及关联查询结果的映射的方法等,因此 MyBaits 中还有很多需要我们深入学习的内容。

下面对 MyBatis 的各个包做一个简要的介绍。

  • org.apache.ibatis.annotations。包含注解方式需要用到的所有注解,主要分为普通的 CRUD 注解、Provider 类注解、配置型注解。

  • org.apache.ibatis.binding。绑定接口和映射语句,使用 JDK 动态代理实现。

  • org.apache.ibatis.builder。映射语句的构造器,包含注解和 XML 两种方式,大量使用建造者模式。

  • org.apache.ibatis.cache。缓存接口和缓存实现,还有许多缓存的装饰类,通过装饰模式提供复杂功能。

  • org.apache.ibatis.cursor。游标接口和实现类,使用游标类型作为返回值可以按需取值。

  • org.apache.ibatis.datasource。数据源相关,提供了 UNPOOLED、POOLED 和 JNDI 三种数据源。

  • org.apache.ibatis.exceptions。异常类,除了这个包,在其他包中也有异常类。

  • org.apache.ibatis.executor。包含了 Executor 接口和几个实现类,二级缓存就是通过 CacheExecutor 装饰类实现的。

这个包还包含以下几个子包。

  • keygen。包含主键生成接口和 3 个实现类,其中 Jdbc3KeyGenerator 依赖于数据库 JDBC 的支持,SelectKeyGenerator 更灵活,通过执行 SQL 获取主键值,NoKeyGenerator 作为默认值不获取主键值。

  • loader。延迟加载相关类,MyBatis 通过对结果进行动态代理(支持 cglib 和 javassist 两种方式)来实现关联和集合的延迟加载,调用特定方法会触发 MyBatis 进行查询。

  • parameter。参数赋值接口,MyBatis 的参数最终会转换为底层的 JDBC 方式,这个接口用于对 JDBC 预编译语句进行赋值。

  • result。ResultHandler 接口的实现类,用于处理映射后的对象,当需要返回 Map 类型的结果时会更有用。

  • resultset。ResultSetHandler 接口的实现类,用于处理 ResultSet 和结果映射类型的转换。DefaultResultSetHandler 中包含了结果映射方法和复杂的嵌套集合类型的处理方法,大量的迭代和互相调用使得这个类很难被理解,建议在阅读这个类的代码时,参考简单(或嵌套集合)的示例,在该类代码中设置断点追踪查看。

  • statement。StatementHandler 接口和相应的实现,是对 JDBC Statement 接口的封装,支持普通方式调用、预编译方式调用、存储过程方式调用。

  • org.apache.ibatis.io。这个包中最主要的两个类是 Resources 和 ResolverUtil,用于获取资源,根据指定条件获取类。

  • org.apache.ibatis.jdbc。JDBC 工具类,包含用于执行脚本的 ScriptRunner 以及在 Provider 注解方式中拼接 SQL 时常用的 SQL 类。

  • org.apache.ibatis.lang。包含两个标记注解,可以在 JUnit 测试时通过 Category 区分执行。

  • org.apache.ibatis.logging。日志接口和常用日志组件的实现,还包含了对 JDBC 底层 Connection、Statement 等对象的日志代理。

  • org.apache.ibatis.mapping。包含了 ResultMap、ParameterMap 等与映射相关的配置,还有对底层 JDBC 的封装。

  • org.apache.ibatis.parsing。XML 解析实现,可以用于 Mapper.xml 和 MyBatis 配置文件的解析。

  • org.apache.ibatis.plugin。包含了与插件相关的接口和注解,以及动态代理的工具类。

  • org.apache.ibatis.reflection。反射工具类,也是 MyBatis 结果映射最重要的工具类。在 MyBatis 中开发插件时,可以使用 SystemMetaObject 工具类创建 MetaObject 对象,使用这个工具可以很方便地获取和修改对象的值,这是一个很强大的工具类。另外 ParamNameUtil 支持在使用 JDK8 时直接获取参数名(可以省略 @Param 注解),在使用该工具类之前可以通过 JDK.parameterExists 判断当前环境是否为 JDK8 以上。

  • org.apache.ibatis.scripting。XML 映射语句实现类,MyBatis 通过自己的一套 XML 标记实现了动态 SQL。如果想要使用其他模板语言,可以参考这个包的代码实现 LanguageDriver 接口。

  • org.apache.ibatis.session。SqlSession 接口及实现类,还有主要功能类。这个包用于对 Executor 和 MappedStatement 进行封装。

  • org.apache.ibatis.transaction。事务接口和实现类,提供了 JDBC 事务和外部的事务管理。

  • org.apache.ibatis.type。Java 类型和 JDBC 类型转换器,MyBatis 在对参数和结果进行转换时,通过这些类型转换器来实现,当需要实现自己的类型转换器时,可以参考此处介绍的大量示例。

以上内容是对各个包的大概介绍,在阅读源码或者学习 MyBatis 时,还有比源码更有价值的内容,那就是测试。MyBatis 针对各个具体的类和功能提供了全面的测试,下一节将通过一个简单的测试示例带领大家了解 MyBatis 的测试用法。