亲自啃了一周,终于把 Mybatis 源码理清,以后简历请写精通二字 - 今日头条

本文由 简悦 SimpRead 转码, 原文地址 www.toutiao.com

啃了一周 tkmybatis 源码,51 张图,5 个主要流程,构成了 tkmybatis 的源码组成。tkmybatis 包括了 mybatis 的部分,源码相

啃了一周 tkmybatis 源码,51 张图,5 个主要流程,构成了 tkmybatis 的源码组成。tkmybatis 包括了 mybatis 的部分,源码相比于 mybatis 多了个 mapperscan 的注解处理,其余部分是一致的。理清了 tkmybatis,就理清了 mybatis 源码,同时对 mybatis 的机制能有更深刻的认识。

纯 mybatis 每个持久化操作都要写 sql, 会显得有些繁琐。现在市面上也有很多的插件, 比如 mybatis 逆向工程, mybatisCodeHelperPro 等, 可以在 xml 文件中生成一些常用的 sql 和对应的 mapper 接口方法。也有一些 mybatis 的第三方工具框架, 帮我们免去单表操作的 sql 编写, 比如 tkmybatis。tkmybatis 源码版本:

1
2
3
4
5
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>

一、tkmybatis 使用案例

mybatis 系列 - 5 分钟教你提升 CRUD 开发效率 200%

二、整体流程

  • 根据 properties 文件中配置的 xml 位置,为每个 mapper 接口生成一个 MappedStatement 对象 (此时对象的 SQLSource 为不可执行的 Provider)
  • 根据 ScanMapper 接口, 将接口加入到 Spring 容器中,创建对应的 BeanDefinition,然后修改 BeanDefinition 中的 Bean 类型为 MapperFactoryBean
  • Spring 容器实例化 MapperFactoryBean 实例,在初始化方法中,获取第一步每个接口生成的 MappedStatement 对象,并将 MappedStatement 对象的 SQLSource 修改为可执行的 SqlSource(Provider 生成 xml 格式的 SQL,根据该 SQL 利用 languageDriver 创建新的 sqlSource)
  • 将 mapper 的 bean 实例注入到需要依赖该实例的 service 中,此时会 Spring 容器调用 MapperFactoryBean 的 getObject 方法,该方法创建了一个 mapper 的代理类(使用 jdk 的动态代理方法),并注入到 service 里面去
  • 执行 SQL 时,对 mapper 的 SQL 操作,都由 mapper 的代理类 mapperProxy 来实现,实现过程跟原生 mybatis 用 sqlsession 创建的代理类一样的,只是这里没有显示的使用 sqlsession 来创建代理类,而是放到了 MapperFactoryBean 的 getObject 里面,将生成的代理类注入给 service

a、接口扫描入口位置

入口 @MapperScan

https://p26.toutiaoimg.com/origin/pgc-image/8e11f1c4fc3c4dd282973dfe21c5221d?from=pc

这个注解会 @Import 进来一个 tk.mapper 的扫描器(将 MapperScannerRegistrar 导入到到 Spring 容器中,并将其声明成一个 bean,该类的功能是处理注解 MapperScan,具体过程见后面)

https://p26.toutiaoimg.com/origin/pgc-image/6b833b34cb1b4def8b5dd789770e8cf5?from=pc

MapperScannerRegistrar 实现了 spring 的 ImportBeanDefinitionRegistrar 接口, 并实现了 registerBeanDefinitions 方法。【注:如下图 Spring 容器在初始化的时候,会先扫描基本注解如 controller 等,然后扫描第三方 jar 包中的组件,扫描完成后再找到实现了 ImportBeanDefinitionRegistrar 接口的 Bean,并将当前的 AnnotationMetadata 和 BeanDefinitionRegistry 作为参数传入,通过该 Bean 扫描自定义注解的组件进来】

https://p26.toutiaoimg.com/origin/pgc-image/d6d6030057cb487b9d755e2d2641d917?from=pc

该 bean 在实例化的时候, 会调用 registerBeanDefinitions 方法来扫描并导入 mapper 接口,接着来看下扫描过程。

https://p26.toutiaoimg.com/origin/pgc-image/e5c24cff97754bc5b0f12278071d79e0?from=pc

b、创建扫描器并利用 MapperScan 参数初始化该扫描器

该步骤创建一个扫描器, 利用 MapperScan 参数初始化该扫描器,各参数意义后面说,默认为空也没关系。同时还会根据 MapperScan 参数确定扫描 package 的范围。

https://p26.toutiaoimg.com/origin/pgc-image/7d87f693e81849e8b797f76e8f572575?from=pc

https://p26.toutiaoimg.com/origin/pgc-image/12212d47f9d041698a9016892f40148d?from=pc

之所以把这个单独提出来,是因为这一步决定了 scanner 能扫描出哪些 mapper,符合 filter 条件的就会被扫描出来。

https://p26.toutiaoimg.com/origin/pgc-image/3c8ddbe413ac4b7ea92c2ad17b519ed9?from=pc

在 registerFilters 中,扫描器会添加 includeFilter 和 excludeFilter,如果扫描出来的类在某一个 excludeFilter 中,则放弃该类,如果扫描出来的类在某一个 includeFilter 中,则保存该类到扫描的返回结果中,excludeFilter 要先于 includeFilter 进行判断。详见( ClassPathScanningCandidateComponentProvider 的 isCandidateComponent 方法)。

下图中 annotationClass 和 markerInterface 都是在前面初始化扫描器的时候,通过 MapperScan 注解传入的。

https://p26.toutiaoimg.com/origin/pgc-image/8ffb6f829cf742d4b48077f369432b01?from=pc

https://p26.toutiaoimg.com/origin/pgc-image/95b39231a4f94390b601966841432268?from=pc

d、调用 doScan 方法扫描 mapper

https://p26.toutiaoimg.com/origin/pgc-image/eac55e4907064adf8b1c469a176c56ff?from=pc

ClassPathMapperScanner 这个扫描器继承了 ClassPathBeanDefinitionScanner 接口,并重写了 doScan 方法和 isCandidateComponent

doScan:里面会直接复用父类自带的 doScan 方法, 因为这就是 spring 扫描包中的 bean 的方法,在该方法中,会先使用上一步调动 registerFilters 方法注册的过滤器判断扫描出来的类是否符合条件(excludefilter 和 includefilter 判断),然后再利用重写后的 isCandidateComponent 方法进一步判断是否符合条件(默认接口类是不符合 Spring 扫描条件的,这里通过重写该方法,判断扫描出来的类是不是接口且该类 metadata 中的 encloseclass 值为 null,是的话就符合扫描条件),两个判断都通过,则认为是扫描出来的 mapper 接口类。

https://p26.toutiaoimg.com/origin/pgc-image/5dc221ba0d4441729535b5f87a72e16f?from=pc

重写的 isCandidateComponent 方法

https://p26.toutiaoimg.com/origin/pgc-image/c1f27e6299374c02a521f6f56ba651f7?from=pc

调用父类的 doScan 后,会扫描到 basePackage 指定包下面的 mapper 接口,并封装成 BeanDefinitionHolder 的集合。BeanDefinitionHolder 包含了 BeanDefinition,同时包括 BeanDefinition 的名称和别名

https://p26.toutiaoimg.com/origin/pgc-image/132af7bedf6f4b7ab21ac1a466deb260?from=pc

processBeanDefinitions 主要做了一下几个处理:

https://p26.toutiaoimg.com/origin/pgc-image/501d083b3598493d9dbee04f5ed6d5fc?from=pc

https://p26.toutiaoimg.com/origin/pgc-image/05b08203dd114ccabf39ae280e3fdb40?from=pc

上图处理中,最重要的就两个,一是更改 BeanClass, 二是设置 autowired-mode = by type, 使得 SqlSeesionTemplate 可以作为 MapperFactoryBean 的属性注入进来(autowired-mode 常见的有三种, AUTOWIRE_NO、AUTOWIRE_BY_NAME、AUTOWIRE_BY_TYPE,是基于 xml 的 Spring 配置时,用来定义 Bean 的注入方式的。利用 @Component 等注解创建的 Bean 默认值都是 AUTOWIRE_NO,表示无需进行属性的注入,有 @autowire 等注解时按注解的方式完成属性注入,AUTOWIRE_BY_NAME 和 AUTOWIRE_BY_TYPE 分别表示按名称和按类型进行参数的注入。如果该属性为这两个值,Spring 容器在创建 Bean 的时候会对 Bean 的属性自动完成注入,注入时会扫描 set 方法,调用 set 方法并对 set 方法的参数注入,从而实现属性的注入。注意,这里因为是扫描出来的 Mapper 类的 Bean 对象且没有 @Component 等注解,所以就通过修改 autowired-mode 的方式,通知 Spring 容器,对该 Bean 属性进行注入)。

好了, 扫描 Mapper 的工作到此为止, 接下来就是 Mapper 接口的实例化了。

上面讲到 Mapper 接口的 BeanDefinition 的 BeanClass 被改成了 tk.mybatis 中的 MapperFactoryBean。那么实例化的工作主要会由这个类 (实例化出另外一个 Bean 来替代原始的 Mapper 接口的 Bean) 来完成。下面是这个类的依赖关系图。

https://p26.toutiaoimg.com/origin/pgc-image/9ebbcc5fd36c49f6a76f4f96c848352f?from=pc

在图中,MapperFactoryBean 集成的 DaoSupport 类实现了 InitializingBean 接口, 那么 spring 在完成属性注入后,会调 DaoSupport 的 afterPropertiesSet 方法。在该 afterPropertiesSet 中调用了 checkDaoConfig 方法,由于 MapperFactoryBean 重写了 checkDaoConfig 方法,所以在 Bean 属性注入完成后,会调用 MapperFactoryBean 的 checkDaoConfig 方法。

https://p26.toutiaoimg.com/origin/pgc-image/93fad33dcf62403691b29094840fb06d?from=pc

https://p26.toutiaoimg.com/origin/pgc-image/a1de6797014941099ce5140ff9a54bf4?from=pc

a、关键属性注入

下面来看看 MapperFactoryBean 的 checkDaoConfig 方法都做了些什么。但是看之前,我们先简单了解下该 Bean 中注入的主要属性,这些属性在 checkDaoConfig 方法中用到了,如果不讲清楚的话,会很疑惑这些属性是从哪里来的,了解了才能对 tk-mybatis 与 Spring 的集成更加的清楚。

mapperInterface 的注入:

https://p26.toutiaoimg.com/origin/pgc-image/6b76363baba746fd8734a5ee1272c620?from=pc

SqlSession 的注入

https://p26.toutiaoimg.com/origin/pgc-image/33066d790b434cdba61e9741534f943b?from=pc

那么被注入的 SqlSessionFactory 和 SqlSessionTemplate 是如何被创建的呢,看下图,玄机就在 mybatis-spring-boot-starter 的依赖包中的 mybatis-spring-boot-autoconfigure:

https://p26.toutiaoimg.com/origin/pgc-image/9b9710dec0884b149899cb5310b15e5d?from=pc

查看该 jar 包 mapper-spring-boot-autoconfigure-2.1.5.jar,可以发现在 META-INF 下有个 spring.factories 文件。

https://p26.toutiaoimg.com/origin/pgc-image/4ad092e86b7146d39e30ca8932038286?from=pc

该配置利用 Spring 的组件扫描机制,配置了 Spring 加载 jar 包后需要扫描的 jar 包内的配置类。该机制可以参考《004-java 基础 - 02-Spring 类 SPI 机制,如何将 jar 包中的类加载到 BeanFactory 中进行管理》。文件内容如下:

https://p26.toutiaoimg.com/origin/pgc-image/ab61c9cfc16e4526809df2f00f8330b5?from=pc

我们看下这个类,这个类被 Spring 加载进来后会做很多事情:

https://p26.toutiaoimg.com/origin/pgc-image/534b25e2d4834894a557267d769b3124?from=pc

创建 SqlSessionFactory 的 Bean:

https://p26.toutiaoimg.com/origin/pgc-image/62d8a428f2b347c191d68a83c17aff60?from=pc

创建 SqlSessionTemplate 的 Bean:

https://p26.toutiaoimg.com/origin/pgc-image/d5bbc504bdac47a0b0c525839763726a?from=pc

到这里,MapperFactoryBean 的关键属性的注入讲完了。

b、SqlSessionFactory 初始化操作内容

前面讲到 MapperFactoryBean 自动注入了 SqlSessionFactory 的 Bean,为了讲清楚 MapperFactoryBean 在属性注入后执行的 checkDaoConfig 操作,我们先了解下 SqlSessionFactory 初始化的时候都做了什么事情。tkmybatis 中 SqlSessionFactory 的初始化基本就是调用 mybatis 的初始化过程。简要概括为以下几点:

  • 读取 Properties 配置文件,提取 mybatis 相关配置
  • 解析 xml 文件,将 xml 文件 mapper 节点中的 mapper 类解析并存放到 mapperRegistr 中,作为 knowsmapper
  • 根据 mapper 的接口方法,为每一个方法创建一个对应的 mappedStatement(敲黑板,checkDaoConfig 操作主要就是为了调整它),创建 mappedStatement 时会解析 mapper 中的 method 方法,创建对应的 SqlSource(根据注解类型判断,如果是 insert 等基本注解,则利用 LanguageDriver 和注解上的 SQL 语句来创建 SqlSource,如果是 Provider 等注解,tkMybatis 就是这一类,则创建的 SqlSource 为 ProviderSqlSource)

下面从源码里面看下这几部分:

https://p26.toutiaoimg.com/origin/pgc-image/111aed42bed645e088befc8096fd84b1?from=pc

factory.getObject() 方法跟下去,发现接着会用加载配置的 xml 文件,生成 Resource 列表,然后遍历处理列表中的 Resource

https://p26.toutiaoimg.com/origin/pgc-image/6d89ab524d3648cbbdf4418cc5bf5428?from=pc

xmlMapperBuilder.parse() 方法跟下去,发现是调用了 Mybatis 中 Configuration 的 addMapper 方法(实现中又是直接调用的 MapperRegistry 的 addMapper 方法),处理每一个 mapper 类,将其添加到 mapper 注册中心。

https://p26.toutiaoimg.com/origin/pgc-image/c4173118e40b4f9abdd445d290ada510?from=pc

https://p26.toutiaoimg.com/origin/pgc-image/5b2e43c483914fd690d53713a3fc6c97?from=pc

再进一步,看 addMapper 方法是如何处理 mapper 类,注意这里 knownMappers.put 方法很重要,knowMappers 中保存了根据 xml 配置获取到的所有的 mapper 类,这里在 put 的时候,将 mapper 类转换成了对应的代理类工厂 MapperProxyFactory(MapperFactoryBean 在完成初始化后,如果在其他类中需要注入 MapperFactoryBean 的实例,会由 Spring 容器调用它的 getObject 方法创建并注入进去。在 getObject 方法里会调用代理工厂类的实例方法,会利用 jdk 的动态代理技术为 mapper 类实例创建对应的代理类实例,返回给 MapperProxyFactory 的 getObject 方法,随后 MapperProxyFactory 将该代理类实例作为 mapper 类的实例(比如 UserMapper 的实例 userMapper,实际就是 Mapper 的代理类实例),执行 sql 的时候,实际 SQL 调用都是在 MapperProxy 的 invoke 方法中进行处理的)

https://p26.toutiaoimg.com/origin/pgc-image/b97f3cc65ef2415da86bf2df27b1217d?from=pc

https://p26.toutiaoimg.com/origin/pgc-image/62b6fc0f90fa423ea0c4387991c59c64?from=pc

在 parse() 方法中,做如下操作:

https://p26.toutiaoimg.com/origin/pgc-image/00e7b9c94eb246fcaadb1bebda82f308?from=pc

parseStatement 方法完成 MappedStatement 创建,包括根据方法注解创建 SqlSource,然后利用 SqlSource 创建 MappedStatement。

https://p26.toutiaoimg.com/origin/pgc-image/5958c571ebfb43969ebe09d3a85ed4ad?from=pc

https://p26.toutiaoimg.com/origin/pgc-image/de95a0cdc9204b36a768184af59fb23f?from=pc

getSqlSource 方法会根据 mapper 方法上的注解类型,决定返回何种 SqlSource。

https://p26.toutiaoimg.com/origin/pgc-image/d1d2e3ee97c54fccbc2d832c7db0ee6a?from=pc

到这里 mapper 解析的操作就完成了,MapperFactoryBean 中的 configuration 保存了每一个 mapper 方法的 mappedStatement 以及已知的 mapper 类。

c、为每个方法对应的 MappedStatement 更新 SqlSource

我们再回到 MapperFactoryBean 类,来看看它的 checkDaoConfig 方法都做了些什么。先把结论说了吧,checkDaoConfig 会根据 mapper 接口将每个接口方法对应的 sqlSource,由前面说的 ProviderSqlSource 转换成 languageDriver 创建的 sqlSource(可以理解为 tkMybatis 根据每个方法的定义,利用 Provider 为每个方法在对应的 mapper.xml 中添加了方法对应的 SQL 语句,然后利用修改后的 xml 创建新的 sqlSource,更新到 MappedStatement 里面去,只有更新后的 MappedStatement,才能执行 Mybatis 的 SQL 处理。当然实际上是没有直接修改 xml 文件这一步的,只是打个比方,可以看后面的代码实现)。

https://p26.toutiaoimg.com/origin/pgc-image/8a689dba85684a5d86958fe39a5118a3?from=pc

再继续看 processConfiguration 方法做了什么操作。

https://p26.toutiaoimg.com/origin/pgc-image/3d9e66e304fe44399b7385851f7c7df8?from=pc

再继续看 processMappedStatement 对 MappedStatement 的处理,该处理中会对 MappedStatement 中的 SqlSource 进行替换。

https://p26.toutiaoimg.com/origin/pgc-image/840bd06b7beb480788f8d57f3439ec5e?from=pc

看一下替换前,MappedStatement 中的 SqlSource 如下图所示,是个 Provider 实现类。

https://p26.toutiaoimg.com/origin/pgc-image/4a546ed9d4e74333b3c594cce72577bb?from=pc

执行 setSqlSource 后,在下图中可以看见,MappedStatement 中的 sqlSource 参数已经变成了 DynamicSqlSource。

https://p26.toutiaoimg.com/origin/pgc-image/42d09d7acfda45eba191d6b34458fdfd?from=pc

接下来,我们继续看下 setSqlSource 到底是怎么改变 sqlSource 的。

https://p26.toutiaoimg.com/origin/pgc-image/254b9e5ec55e4a7481ae15913dffd57a?from=pc

继续看 mapperTemplate 的 setSqlSource 方法,该方法完成了 xml 形式 sql 的创建和对应 sqlSource 的创建。

https://p26.toutiaoimg.com/origin/pgc-image/8c1316dd99764de3b4c38952cfacc6f4?from=pc

跟进 method.invoke 方法,可以发现,该方法实际是在 mapper 接口方法对应的 Provider 实现类中执行的,如下图 selectByExample 方法由 ExampleProvider 来生成 xmlsql。

https://p26.toutiaoimg.com/origin/pgc-image/85baddfc1c7246dabfddbb5781aa59b2?from=pc

至此,tkMybatis 就在 checkDaoConfig 方法中将接口方法对应的 MappedStatement 中的 sqlSource 由不能执行的 Provider 实现类,转换成了 Mybatis 中的可执行的 sqlSource(根据 xmlSql 生成,和 mybatis 扫描 mapper.xml 文件中创建的 MapperStatement 和 SqlSource 具有同样的功效!)

到目前为止,经过 1、Mapper 接口扫描和 2、Mapper 接口 Bean 的实例化就完成了 tkMybatis 的初始化工作。

3、将 Mapper 实例注入到依赖该实例的 bean 中

前面 tkMybatis 做了两件事情,一是扫描 Mapper 接口比如 UserMapper,创建对应的 Bean 定义,并将其中的类型修改为 MapperFactoryBean;二是实例化该 Bean,因为类型已经修改为 MapperFactoryBean,就完成 MapperFactoryBean 的实例化和初始化。

接下来还有关键的一步,怎么用这个 Bean,我们在写 service 的时候,会通过 @autowired 注解将 Mapper 注入到 service 中,比如下面的:

https://p26.toutiaoimg.com/origin/pgc-image/31c0df8e88654047852d890c6df07c3e?from=pc

以 UsersMapper 举个例子,Spring 在注入 UsersMapper 时,会调用对应 MapperFactoryBean 实例的 getObject 方法,如下图,在该方法中,会创建 UsersMapper 的的代理类,进入 getmapper 这个方法,会跟到前面第 2 部分 knowMappers 部分,getmapper 是从 knowMappers 获取的 Mapper,该 Mapper 是一个 Mapper 接口的代理类 MapperProxy,对 UserMapper 的方法调用都会有 MapperProxy 的 invoke 方法来实现。

这也就解决了 “明明是 Mapper 的接口比如 UserMapper,但是在 bean 定义中把它的类型改成了 MapperFactoryBean,MapperFactoryBean 又没有实现 Mapper 的接口方法,那么 service 中注入的 mapper 调用方法的时候是如何调用的问题”。

https://p26.toutiaoimg.com/origin/pgc-image/edfebad4589f48e39963dfd45a8156c8?from=pc

4、实际 SQL 语句的执行

接着上一部分,调试下实际执行 SQL 语句的过程。

https://p26.toutiaoimg.com/origin/pgc-image/55eda036e3e64e52b76a733b73e69f30?from=pc

https://p26.toutiaoimg.com/origin/pgc-image/502f6c7fbdb244e99d457ebbf2f020c7?from=pc

其实,像下图一样,使用经过注入后的 Mapper 代理类,就跟原生 Mybatis 创建的代理类是一样的,执行的 SQL 操作的过程也是一样的。里面的每个接口方法也都有对应的 MappedStatement 实现。如果对 invoke 代理的过程感兴趣,可以调试跟进去看,看看如何 invoke 中如何用 MappedStatement 做具体的 SQL 处理,这里就不具体说了。

https://p26.toutiaoimg.com/origin/pgc-image/d74f78e1c93545768068886251071120?from=pc

和 mybatis 的关系:

  1. 在不影响 mybatis 原有功能的情况下, 很好的拓展了 mybatis 的功能
  2. 扫描 xml 的工作依旧由 mybatis 来完成, 再次扫描并注册 mapper 接口的功能以拓展的方式由 tk.mapper 复写, 扫描后的结果依旧存放在 mybatis 的 Configuration 中,和 mybatis 自己扫描 mapper 接口的代码逻辑几乎一致, 唯一添加的功能就是, 对 mybatis 的 Configuration 中的由自己拓展的方法对应的 MapperStatement 的 sqlSource 进行更改, 以此来提供具体可执行 sql
  3. 后续执行持久化方法, 依然是 mybatis 的代码功能, tk.mapper 仅在扫描 mapper 接口阶段提供了 SqlSource