MyBatis 核心处理层:核心配置文件与映射配置文件解析流程

很多开源框架之所以能够流行起来,往往是因为解决了一些领域内的通用问题,但在实际使用中我们一般只需要解决特定的问题,这时就需要有一种机制来控制开源框架的行为,这也是大多数开源框架提供各种各样配置的主要原因之一。控制开源框架行为的主流方式有 XML注解两种,本文重点讲解 XML 配置方式。

MyBatis 有两种 XML 配置,一个是 核心配置文件中的整体配置,另一个是 mapper.xml 文件中的映射配置。在初始化的过程中,MyBatis 会读取核心配置文件以及所有的 mapper.xml 映射配置文件,同时还会加载这两个配置文件中指定的类,解析类中的相关注解,最终将解析得到的信息转换成配置对象。完成配置加载之后,MyBatis 就会根据得到的配置对象初始化各个模块。

1 构造者模式

MyBatis 在加载配置文件、创建配置对象的时候,会使用到经典设计模式中的构造者模式

构造者模式最核心的思想就是将创建复杂对象的过程与复杂对象本身进行拆分。通俗来讲,构造者模式是将复杂对象的创建过程分解成了多个简单步骤,在创建复杂对象的时候,只需要了解复杂对象的基本属性即可,而不需要关心复杂对象的内部构造过程。这样的话,使用方只需要关心这个复杂对象要什么数据,而不再关心内部细节。

构造者模式的类图如下所示:

构造者模式类图

从图中,我们可以看到构造者模式的四个核心组件:

  • Product 接口:复杂对象的接口,定义了要创建的目标对象的行为。
  • ProductImplProduct 接口的实现,它真正要创建的复杂对象,其中实现了我们需要的复杂业务逻辑。
  • Builder 接口:定义了构造 Product 对象的每一步行为。
  • BuilderImplBuilder 接口的具体实现,其中具体实现了构造一个 Product 的每一个步骤,例如上图中的 setPart1()setPart2() 等方法,都是用来构造 ProductImpl 对象的各个部分。在完成整个 Product 对象的构造之后,我们会通过 build() 方法返回这个构造好的 Product 对象。

使用构造者模式一般有两个目的:

  • 第一个目的是将使用方与复杂对象的内部细节隔离,从而实现解耦的效果。使用方提供的所有信息,都是由 Builder 这个“中间商”接收的,然后由 Builder 消化这些信息并构造出一个完整可用的 Product 对象。
  • 第二个目的是简化复杂对象的构造过程。在很多场景中,复杂对象可能有很多默认属性,这时我们就可以将这些默认属性封装到 Builder 中,这样就可以简化创建复杂对象所需的信息。

通过构建者模式的类图我们还可以看出,每个 BuilderImpl 实现都是能够独立创建出对应的 ProductImpl 对象,那么在程序需要扩展的时候,我们只需要添加新的 BuilderImplProductImpl,就能实现功能的扩展,这完全符合“开放 —— 封闭“原则。

2 核心配置文件解析流程

MyBatis 初始化的第一个步骤就是加载和解析核心配置文件

入口是 XMLConfigBuilder 这个 Builder 对象,它由 SqlSessionFactoryBuilder.build() 方法创建:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class SqlSessionFactoryBuilder {
  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = 
        new XMLConfigBuilder(reader, environment, properties);
      // 会根据得到的 Configuration 全局配置对象
      //  创建一个 DefaultSqlSessionFactory 对象
      return build(parser.parse());
    } //  catch finally ...
  }
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }
}

XMLConfigBuilder 会解析核心配置文件得到对应的 Configuration 全局配置对象,然后 SqlSessionFactoryBuilder 会根据得到的 Configuration 全局配置对象创建一个 DefaultSqlSessionFactory 对象返回给上层使用。

这里创建的 XMLConfigBuilder 对象的核心功能就是解析核心配置文件XMLConfigBuilder 有一部分能力继承自 BaseBuilder 抽象类,具体继承关系如下图所示:

BaseBuilder 继承关系图

BaseBuilder 抽象类扮演了构造者模式中 Builder 接口的角色,下面我们先来看 BaseBuilder 中各个字段的定义:

  • Configuration configuration:MyBatis 的初始化过程就是围绕 Configuration 对象展开的,我们可以认为 Configuration 是一个单例对象,MyBatis 初始化解析到的全部配置信息都会记录到 Configuration 对象中。
  • TypeAliasRegistry typeAliasRegistry:用于注册别名 typeAlias。
  • TypeHandlerRegistry typeHandlerRegistry:用于注册自定义类型转换器 TypeHandler

除了关联 Configuration 对象之外,BaseBuilder 还提供了另外两个基本能力:

  • 解析别名:核心逻辑是在 BaseBuilder.resolveAlias() 方法中实现的,主要依赖于 TypeAliasRegistry 对象;
  • 解析 TypeHandler:核心逻辑是在 BaseBuilder.resolveTypeHandler() 方法中实现的,主要依赖于 TypeHandlerRegistry 对象。

了解了 BaseBuilder 提供的基础能力之后,我们回到 XMLConfigBuilder 这个 Builder 实现类,看看它是如何解析核心配置文件的。该类有以下四个核心字段:

  • boolean parsed:状态标识字段,记录当前 XMLConfigBuilder 对象是否已经成功解析完核心配置文件。
  • XPathParser parserXPathParser 解析器,用于解析核心配置文件。
  • String environment:标签定义的环境名称。
  • ReflectorFactory localReflectorFactory:创建和缓存 Reflector 对象。

在上文介绍的 SqlSessionFactoryBuilder.build() 方法中可以看到,XMLConfigBuilder.parse() 方法触发了核心配置文件的解析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public Configuration parse() {
  if (parsed) {
    // 如果解析过了,抛出异常
    throw new BuilderException("...");
  }
  parsed = true; // 设置解析状态
  // 解析 configuration 
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}

2.1 处理 properties 标签

我们可以通过 <properties> 标签定义 KV 信息供 MyBatis 使用,propertiesElement() 方法的核心逻辑就是解析核心配置文件中的 <properties> 标签。

<properties> 标签中解析出来的 KV 信息会被记录到一个 Properties 对象(也就是 Configuration 全局配置对象的 variables 字段),在后续解析其他标签的时候,MyBatis 会使用这个 Properties 对象中记录的 KV 信息替换匹配的占位符。

其中的 parseConfiguration() 方法定义了解析核心配置文件的完整流程,源码如下:

parseConfiguration

parseConfiguration() 方法中,我们可以清晰地看到 XMLConfigBuilder 对核心配置文件中各类标签的解析方法,下面我们就逐一介绍这些方法的核心实现。

2.2 处理 settings 标签

MyBatis 中有很多全局性的配置,例如,是否使用二级缓存、是否开启懒加载功能等,这些都是通过核心配置文件中的 <settings> 标签进行配置的。

settingsAsProperties() 方法的核心逻辑就是解析 <settings> 标签,并将解析得到的配置信息记录到 Configuration 这个全局配置对象的同名属性中。

2.3 处理 typeAliases 和 typeHandlers 标签

XMLConfigBuilder 中提供了 typeAliasesElement() 方法和 typeHandlerElement() 方法,分别用来负责处理 <typeAliases> 标签和 <typeHandlers> 标签,解析得到的别名信息和 TypeHandler 信息就会分别记录到 TypeAliasRegistryTypeHandlerRegistry

2.4 处理 plugins 标签

MyBatis 是一个非常易于扩展的持久层框架,而插件就是 MyBatis 提供的一种重要扩展机制。

我们可以自定义一个实现了 Interceptor 接口的插件来扩展 MyBatis 的行为,或是拦截 MyBatis 的一些默认行为。这里我们重点来看 MyBatis 初始化过程中插件配置的加载,也就是 XMLConfigBuilder 中的 pluginElement() 方法,该方法的核心就是解析 <plugins> 标签中配置的自定义插件。

2.5 处理 objectFactory 标签

在之前的 MyBatis 反射工具模块 中我们提到过 MyBatis 提供了一个 ObjectFactory 的默认实现类 DefaultReflectorFactory,此外 MyBatis 也支持自定义 ObjectFactory 实现类。

XMLConfigBuilder 中的 objectFactoryElement() 方法就实现了加载自定义 ObjectFactory 实现类的功能,其核心逻辑就是解析 <objectFactory> 标签中配置的自定义 ObjectFactory 实现类,并完成相关的实例化操作。

除了 <objectFactory> 标签之外,我们还可以通过 <objectWrapperFactory> 标签和 <reflectorFactory> 标签配置自定义的 ObjectWrapperFactory 实现类和 ReflectorFactory 实现类,这两个标签的解析分别对应 objectWrapperFactoryElement() 方法和 reflectorFactoryElement() 方法,两者实现与 objectFactoryElement() 方法实现类似,这里就不再展示。

2.6 处理 environments 标签

在 MyBatis 中,我们可以通过 <environment> 标签为不同的环境添加不同的配置,例如,线上环境、预上线环境、测试环境等,每个 <environment> 标签只会对应一种特定的环境配置。

environmentsElement() 方法中实现了 XMLConfigBuilder 处理 <environments> 标签的核心逻辑,它会根据 XMLConfigBuilder.environment 字段值,拿到正确的 <environment> 标签,然后解析这个环境中使用的 TransactionFactoryDataSource 等核心对象,也就知道了 MyBatis 要请求哪个数据库、如何管理事务等信息。

2.7 处理 databaseIdProvider 标签

在 MyBatis 中编写的都是原生的 SQL 语句,而很多数据库产品都会有一些 SQL 方言,这些方言与标准 SQL 不兼容。

在核心配置文件中,我们可以通过 <databaseIdProvider> 标签定义需要支持的全部数据库的 DatabaseId,在后续编写 Mapper 映射配置文件的时候,就可以为同一个业务场景定义不同的 SQL 语句(带有不同的 DataSourceId),来支持不同的数据库,这里就是靠 DatabaseId 来确定哪个 SQL 语句支持哪个数据库的。

databaseIdProviderElement() 方法是 XMLConfigBuilder 处理 <databaseIdProvider> 标签的地方,其中的核心就是获取 DatabaseId

databaseIdProviderElement

由上可知,解析 <databaseIdProvider> 标签之后会得到一个 DatabaseIdProvider 对象,其核心方法是 getDatabaseId() 方法,主要是根据前面解析得到的 DataSource 对象来生成 DatabaseIdDatabaseIdProvider 的继承关系如下所示:

DatabaseIdProvider 继承关系图

从继承关系图中可以看出,DefaultDatabaseIdProvider 是个空实现,而且已被标记为过时了,所以这里我们就重点来看 VendorDatabaseIdProvider 实现。

VendorDatabaseIdProvidergetDatabaseId() 方法中,首先会从 DataSource 中拿到数据库的名称,然后根据 <databaseIdProvider> 标签配置和 DataSource 返回的数据库名称,确定最终的 DatabaseId 标识。

2.8 处理 mappers 标签

除了全局的核心配置文件之外,MyBatis 初始化的时候还会加载 <mappers> 标签下定义的 Mapper 映射文件,<mappers> 标签中会指定 mapper.xml 映射文件的位置。

XMLConfigBuilder 处理 <mappers> 标签的具体实现是 mapperElement(),其中会初始化 XMLMapperBuilder 对象来加载各个 mapper.xml 映射文件。同时,还会扫描 Mapper 映射文件相应的 Mapper 接口,处理其中的注解并将 Mapper 接口注册到 MapperRegistry 中。

3 映射配置文件解析流程

上文我们介绍过,核心配置文件中可以定义多个 <mapper> 标签指定 Mapper 配置文件的地址,MyBatis 会为每个 mapper.xml 映射文件创建一个 XMLMapperBuilder 实例完成解析。

XMLConfigBuilder 类似,XMLMapperBuilder 也是具体构造者的角色,继承了 BaseBuilder 这个抽象类,解析 mapper.xml 映射文件的入口是 XMLMapperBuilder.parse() 方法,其核心步骤如下:

  • 执行 configurationElement() 方法解析整个 mapper.xml 映射文件的内容;
  • 获取当前 mapper.xml 映射文件指定的 Mapper 接口,并进行注册;
  • 处理 configurationElement() 方法中解析失败的 <resultMap> 标签;
  • 处理 configurationElement() 方法中解析失败的 <cache-ref> 标签;
  • 处理 configurationElement() 方法中解析失败的 SQL 语句标签。

可以清晰地看到,configurationElement() 方法才是真正解析 mapper.xml 映射文件的地方,其中定义了处理 mapper.xml 映射文件的核心流程:

  • 获取 <mapper> 标签中的 namespace 属性,同时会进行多种边界检查;
  • 解析 <cache-ref> 标签;
  • 解析 <cache> 标签;
  • 解析 <parameterMap> 标签;
  • 解析 <resultMap> 标签;
  • 解析 <sql> 标签;
  • 解析 <select><insert><update><delete> 等 SQL 标签。

下面我们就按照顺序逐一介绍这些方法的核心实现:

3.1 处理 cache 标签

Cache 接口及其实现是 MyBatis 一级缓存和二级缓存的基础。其中,一级缓存是默认开启的,而二级缓存默认情况下并没有开启,如有需要,可以通过 <cache> 标签为指定的 namespace 开启二级缓存。

XMLMapperBuilder 中解析 <cache> 标签的核心逻辑位于 cacheElement() 方法中,其具体步骤如下:

  • 获取 <cache> 标签中的各项属性(typeflushIntervalsize 等属性);
  • 读取 <cache> 标签下的子标签信息,这些信息将用于初始化二级缓存;
  • MapperBuilderAssistant 会根据上述配置信息,创建一个全新的 Cache 对象并添加到 Configuration.caches 集合中保存。

也就是说,解析 <cache> 标签得到的所有信息将会传给 MapperBuilderAssistant 完成 Cache 对象的创建,创建好的 Cache 对象会添加到 Configuration.caches 集合中,这个 caches 字段是一个 StrictMap<Cache> 类型的集合,其中的 Key 是 Cache 对象的唯一标识,默认值是 mapper.xml 映射文件的 namespace,Value 才是真正的二级缓存对应的 Cache 对象。

StrictMap 简介

StrictMap 继承了 HashMap,并且覆盖了 HashMap 的一些行为,例如,相较于 HashMapput() 方法,StrictMapput() 方法有如下几点不同:

  • 如果检测到重复 Key 的写入,会直接抛出异常;
  • 在没有重复 Key 的情况下,会正常写入 KV 数据,与此同时,还会根据 Key 产生一个 shortKeyshortKey 与完整 Key 指向同一个 Value 值;
  • 如果 shortKey 已经存在,则将 Value 修改成 Ambiguity 对象,Ambiguity 对象表示这个 shortKey 存在二义性,后续通过 StrictMapget() 方法获取该 shortKey 的时候,会抛出异常。

了解了 StrictMap 这个集合类的特性之后,我们回到 MapperBuilderAssistant 这个类继续分析,在它的 useNewCache() 方法会根据前面解析得到的配置信息,通过 CacheBuilder 创建 Cache 对象。

CacheBuilderCache 的构造者,其中最核心的方法是 build() 方法,其中会根据传入的配置信息创建底层存储数据的 Cache 对象以及相关的 Cache 装饰器,具体实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public Cache build() {
  // 将 implementation 默认值设置为 PerpetualCache,在 decorators 集合中默认添加 LruCache 装饰器,
  // 都是在 setDefaultImplementations() 方法中完成的
  setDefaultImplementations();
  // 通过反射,初始化 implementation 指定类型的对象
  Cache cache = newBaseCacheInstance(implementation, id);
  // 创建 Cache 关联的 MetaObject 对象,并根据 properties 设置 Cache 中的各个字段
  setCacheProperties(cache);
  // 根据上面创建的 Cache 对象类型,决定是否添加装饰器
  if (PerpetualCache.class.equals(cache.getClass())) {
    // 如果是 PerpetualCache 类型,则为其添加 decorators 集合中指定的装饰器
    for (Class<? extends Cache> decorator : decorators) {
      // 通过反射创建 Cache 装饰器
      cache = newCacheDecoratorInstance(decorator, cache);
      // 依赖 MetaObject 将 properties 中配置信息设置到 Cache 的各个属性中,
      //  同时调用 Cache 的 initialize() 方法完成初始化
      setCacheProperties(cache);
    }
    // 根据 readWrite、blocking、clearInterval 等配置,
    // 添加 SerializedCache、ScheduledCache 等装饰器
    cache = setStandardDecorators(cache);
  } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
    // 如果不是 PerpetualCache 类型,就是其他自定义类型的 Cache,
    // 则添加一个 LoggingCache 装饰器
    cache = new LoggingCache(cache);
  }
  return cache;
}

3.2 处理 cache-ref 标签

通过上述介绍我们知道,可以通过 <cache> 标签为每个 namespace 开启二级缓存,同时还会将 namespace 与关联的二级缓存 Cache 对象记录到 Configuration.caches 集合中,也就是说二级缓存是 namespace 级别的。但是,某些场景下,我们需要让多个 namespace 共享同一个二级缓存,也就是共享同一个 Cache 对象。

为了解决这个需求,MyBatis 提供了 <cache-ref> 标签来引用另一个 namespace 的二级缓存。cacheRefElement() 方法是处理 <cache-ref> 标签的核心逻辑所在,在 Configuration 中维护了一个 cacheRefMap 字段(HashMap<String,String> 类型),其中的 Key 是 <cache-ref> 标签所属的 namespace 标识,Value 值是 <cache-ref> 标签引用的 namespace 值,如此便可以将两个 namespace 关联起来了,即这两个 namespace 共用一个 Cache 对象。

这里会使用到一个叫 CacheRefResolverCache 引用解析器。CacheRefResolver 中记录了被引用的 namespace 以及当前 namespace 关联的 MapperBuilderAssistant 对象。前面在解析 <cache> 标签的时候我们介绍过,MapperBuilderAssistant 会在 useNewCache() 方法中通过 CacheBuilder 创建新的 Cache 对象,并记录到 currentCache 字段。而这里解析 <cache-ref> 标签的时候,MapperBuilderAssistant 会通过 useCacheRef() 方法从 Configuration.caches 集合中,根据被引用的 namespace 查找共享的 Cache 对象来初始化 currentCache,而不再创建新的 Cache 对象,从而实现二级缓存的共享。

3.3 处理 resultMap 标签

我们都知道,SELECT 语句执行得到的结果集实际上是一张二维表,而 Java 是一门面向对象的程序设计语言,在使用 JDBC 的时候,我们需要手动写代码将 SELECT 语句的结果集转换成 Java 对象,这是一项重复性很大的操作。为了将 Java 开发者从这种重复性的工作中解脱出来,MyBatis 提供了 <resultMap> 标签来定义结果集与 Java 对象之间的映射规则。

首先,<resultMap> 标签下的每一个子标签,例如:<column><id> 等,都会被解析为一个 ResultMapping 对象,其中维护了数据库表中一个列与对应 Java 类中一个属性之间的映射关系。下面是 ResultMapping 中核心字段的含义:

  • String column:当前标签中指定的 column 属性值,指向的是数据库表中的一个列名(或是别名)。
  • String property:当前标签中指定的 property 属性值,指向的是与 column 列对应的属性名称。
  • Class<?> javaTypeJdbcType jdbcType:当前标签指定的 javaType 属性值和 jdbcType 属性值,指定了 property 字段的 Java 类型以及对应列的 JDBC 类型。
  • TypeHandler<?> typeHandler:当前标签的 typeHandler 属性值,这里指定的 TypeHandler 会覆盖默认的类型处理器。
  • String nestedResultMapId:当前标签的 resultMap 属性值,通过该属性我们可以引用另一个 <resultMap> 标签的 id,然后由这个被引用的 <resultMap> 标签映射结果集中的一部分列。这样,我们就可以将一个查询结果集映射成多个对象,同时确定这些对象之间的关联关系。
  • String nestedQueryId:当前标签的 select 属性,我们可以通过该属性引用另一个 <select> 标签中的 select 语句定义,它会将当前列的值作为参数传入这个 select 语句。由于当前结果集可能查询出多行数据,那么可能就会导致 select 属性指定的 SQL 语句会执行多次,也就是著名的 N+1 问题
  • String columnPrefix:当前标签的 columnPrefix 属性值,记录了表中列名的公共前缀。
  • String resultSet:当前标签的 resultSet 属性值。
  • boolean lazy:当前标签的 fetchType 属性,表示是否延迟加载当前标签对应的列。

介绍完 ResultMapping 对象(即 <resultMap> 标签下各个子标签的解析结果)之后,我们再来看 <resultMap> 标签如何被解析。整个 <resultMap> 标签最终会被解析成 ResultMap 对象,它与 ResultMapping 之间的映射关系如下图所示:

ResultMap 结构图

通过上图我们可以看出,ResultMap 中有四个集合与 ResultMapping 紧密相连:

  • resultMappings 集合:维护了整个 <resultMap> 标签解析之后得到的全部映射关系,也就是全部 ResultMapping 对象。
  • idResultMappings 集合:维护了与唯一标识相关的映射,例如,<id> 标签、<constructor> 标签下的 <idArg> 子标签解析得到的 ResultMapping 对象。如果没有定义 <id> 等唯一性标签,则由 resultMappings 集合中全部映射关系来确定一条记录的唯一性,即 idResultMappings 集合与 resulMappings 集合相同。
  • constructorResultMappings 集合:维护了 <constructor> 标签下全部子标签定义的映射关系。
  • propertyResultMappings 集合:维护了不带 Constructor 标志的映射关系。

除了上述四个 ResultMapping 集合,ResultMap 中还维护了下列核心字段:

  • idString 类型):当前 <resultMap> 标签的 id 属性值。
  • typeClass 类型):当前 <resultMap>type 属性值。
  • mappedColumnsSet<String> 类型):维护了所有映射关系中涉及的 column 属性值,也就是所有的列名(或别名)。
  • hasNestedResultMapsboolean 类型):当前 <resultMap> 标签是否嵌套了其他 <resultMap> 标签,即这个映射关系中指定了 resultMap 属性,且未指定 resultSet 属性。
  • hasNestedQueriesboolean 类型):当前 <resultMap> 标签是否含有嵌套查询。也就是说,这个映射关系中是否指定了 select 属性。
  • autoMappingBoolean 类型):当前 ResultMap 是否开启自动映射的功能。
  • discriminatorDiscriminator 类型):对应 <discriminator> 标签。

接下来我们开始深入分析 <resultMap> 标签解析的流程。XMLMapperBuilderresultMapElements() 方法负责解析 Mapper 配置文件中的全部 <resultMap> 标签,其中会通过 resultMapElement() 方法解析单个 <resultMap> 标签。

下面是 XMLMapperBuilder.resultMapElement() 方法解析 <resultMap> 标签的核心流程:

  • 获取 <resultMap> 标签的 type 属性值,这个值表示结果集将被映射成 type 指定类型的对象。如果没有指定 type 属性的话,会找其他属性值,优先级依次是:typeofTyperesultTypejavaType。在这一步中会确定映射得到的对象类型,这里支持别名转换。
  • 解析 <resultMap> 标签下的各个子标签,每个子标签都会生成一个 ResultMapping 对象,这个 ResultMapping 对象会被添加到 resultMappings 集合(List<ResultMapping> 类型)中暂存。这里会涉及 <id><result><association><collection><discriminator> 等子标签的解析。
  • 获取 <resultMap> 标签的 id 属性,默认值会拼装所有父标签的 idvalueproperty 属性值。
  • 获取 <resultMap> 标签的 extendsautoMapping 等属性。
  • 创建 ResultMapResolver 对象,ResultMapResolver 会根据上面解析到的 ResultMappings 集合以及 <resultMap> 标签的属性构造 ResultMap 对象,并将其添加到 Configuration.resultMaps 集合(StrictMap 类型)中。

3.3.1 解析 id、result、constructor 标签

resultMapElement() 方法中获取到 id 属性和 type 属性值之后,会调用 buildResultMappingFromContext() 方法解析上述标签得到 ResultMapping 对象。

经过解析得到 ResultMapping 对象集合之后,会记录到 resultMappings 这个临时集合中,然后由 ResultMapResolver 调用 MapperBuilderAssistant 中的 addResultMap() 方法创建 ResultMap 对象,将 resultMappings 集合中的全部 ResultMapping 对象添加到其中,然后将 ResultMap 对象记录到 Configuration.resultMaps 集合中。

至于 <constructor> 标签的流程,是由 XMLMapperBuilder 中的 processConstructorElement() 方法实现,其中会先获取 <constructor> 标签的全部子标签,然后为每个标签添加 CONSTRUCTOR 标志(为每个 <idArg> 标签添加额外的 ID 标志),最后通过 buildResultMappingFromContext() 方法创建 ResultMapping 对象并记录到 resultMappings 集合中暂存,这些 ResultMapping 对象最终也会添加到前面介绍的 ResultMap 对象。

3.3.2 解析 association 和 collection 标签

接下来,我们来介绍解析 <association><collection> 标签的核心流程,两者解析的过程基本一致。前面介绍的 buildResultMappingFromContext() 方法不仅完成了 <id><result> 等标签的解析,还完成了 <association><collection> 标签的解析,其中相关的代码片段如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
    String property;
    // <association> 标签中其他属性的解析与 <result>、<id> 标签类似,这里不再展开
    // 如果 <association> 标签没有指定 resultMap 属性,那么就是匿名嵌套映射,需要通过
    //  processNestedResultMappings() 方法解析该匿名的嵌套映射
    String nestedResultMap = context.getStringAttribute("resultMap", () ->
            processNestedResultMappings(context, Collections.emptyList(), resultType));
    // <association> 标签中其他属性的解析与 <result>、<id> 标签类似,这里不再展开

    // 根据上面解析到的属性值,创建 ResultMapping 对象
    return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, 
        jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, 
        typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}

这里的 processNestedResultMappings() 方法会递归执行 resultMapElement() 方法解析 <association> 标签和 <collection> 标签指定的匿名嵌套映射,得到一个完整的 ResultMap 对象,并添加到 Configuration.resultMaps 集合中。

3.3.3 解析 discriminator 标签

最后一个要介绍的是 <discriminator> 标签的解析过程,我们将 <discriminator> 标签与 <case> 标签配合使用,根据结果集中某列的值改变映射行为。从 resultMapElement() 方法的逻辑我们可以看出,<discriminator> 标签是由 processDiscriminatorElement() 方法专门进行解析的,具体实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) {
  // 从 <discriminator> 标签中解析 column、javaType、jdbcType、typeHandler 四个属性
  String column = context.getStringAttribute("column");
  String javaType = context.getStringAttribute("javaType");
  String jdbcType = context.getStringAttribute("jdbcType");
  String typeHandler = context.getStringAttribute("typeHandler");
  Class<?> javaTypeClass = resolveClass(javaType);
  Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
  JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
  Map<String, String> discriminatorMap = new HashMap<>();
  // 解析 <discriminator> 标签的 <case> 子标签
  for (XNode caseChild : context.getChildren()) {
    String value = caseChild.getStringAttribute("value");
    // 通过前面介绍的 processNestedResultMappings() 方法,解析 <case> 标签,
    // 创建相应的嵌套 ResultMap 对象
    String resultMap = caseChild.getStringAttribute("resultMap", 
        processNestedResultMappings(caseChild, resultMappings, resultType));
    // 记录该列值与对应选择的 ResultMap 的 Id
    discriminatorMap.put(value, resultMap);
  }
  // 创建 Discriminator 对象
  return builderAssistant.buildDiscriminator(resultType, 
    column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
}

4 SQL 语句解析流程

在 mapper.xml 映射文件中,除了上面介绍的标签之外,还有一类比较重要的标签,那就是 <select><insert><delete><update> 等 SQL 语句标签。虽然定义在 mapper.xml 映射文件中,但是这些标签是由 XMLStatementBuilder 进行解析的,而不再由 XMLMapperBuilder 来完成解析。

在开始介绍 XMLStatementBuilder 解析 SQL 语句标签的具体实现之前,我们先来了解一下 MyBatis 在内存中是如何表示这些 SQL 语句标签的。在内存中,MyBatis 使用 SqlSource 接口来表示解析之后的 SQL 语句,其中的 SQL 语句只是一个中间态,可能包含动态 SQL 标签或占位符等信息,无法直接使用。SqlSource 接口的定义如下:

1
2
3
4
public interface SqlSource {
  // 根据 Mapper 文件或注解描述的 SQL 语句,以及传入的实参,返回可执行的 SQL
  BoundSql getBoundSql(Object parameterObject);
}

MyBatis 在内存中使用 MappedStatement 对象表示上述 SQL 标签。在 MappedStatement 中的 sqlSource 字段记录了 SQL 标签中定义的 SQL 语句,sqlCommandType 字段记录了 SQL 语句的类型(INSERTUPDATEDELETESELECTFLUSH 类型)。

XMLStatementBuilder 解析 SQL 标签的入口方法是 parseStatementNode() 方法,在该方法中首先会根据 id 属性和 databaseId 属性决定加载匹配的 SQL 标签,然后解析其中的 <include> 标签和 <selectKey> 标签,相关的代码片段如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void parseStatementNode() {
  // 获取 SQL 标签的 id 以及 databaseId 属性
	String id = context.getStringAttribute("id");
	String databaseId = context.getStringAttribute("databaseId");
    // 若 databaseId 属性值与当前使用的数据库不匹配,则不加载该 SQL 标签
    // 若存在相同 id 且 databaseId 不为空的 SQL 标签,则不再加载该 SQL 标签
	if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
		return;
	}
    // 根据 SQL 标签的名称决定其 SqlCommandType
	String nodeName = context.getNode().getNodeName();
	SqlCommandType sqlCommandType = SqlCommandType
    .valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    // 获取 SQL 类型、是否使用缓存、是否排序等
	Boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
	Boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
	Boolean useCache = context.getBooleanAttribute("useCache", isSelect);
	Boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
	// 在解析 SQL 语句之前,先处理其中的 <include> 标签
	XMLIncludeTransformer includeParser = 
    new XMLIncludeTransformer(configuration, builderAssistant);
	includeParser.applyIncludes(context.getNode());
    // 获取 SQL 标签的 parameterType、lang 两个属性
	String parameterType = context.getStringAttribute("parameterType");
	Class<?> parameterTypeClass = resolveClass(parameterType);
	String lang = context.getStringAttribute("lang");
	LanguageDriver langDriver = getLanguageDriver(lang);
	// 解析 <selectKey> 标签
	processSelectKeyNodes(id, parameterTypeClass, langDriver);
	// 省略后边的。..
}

4.1 处理 include 标签

在实际应用中,我们会在 <sql> 标签中定义一些能够被重用的 SQL 片段,在 XMLMapperBuilder.sqlElement() 方法中会根据当前使用的 DatabaseId 匹配 <sql> 标签,只有匹配的 SQL 片段才会被加载到内存。

在解析 SQL 标签之前,MyBatis 会先将 <include> 标签转换成对应的 SQL 片段(即定义在 <sql> 标签内的文本),这个转换过程是在 XMLIncludeTransformerapplyIncludes() 方法中实现的(其中不仅包含了 <include> 标签的处理,还包含了 “${}” 占位符的处理)。

针对 <include> 标签的处理如下:

  • 查找 refid 属性指向的 <sql> 标签,得到其对应的 Node 对象;
  • 解析 <include> 标签下的 <property> 标签,将得到的键值对添加到 variablesContext 集合(Properties 类型)中,并形成新的 Properties 对象返回,用于替换占位符;
  • 递归执行 applyIncludes() 方法,因为在 <sql> 标签的定义中可能会使用 <include> 引用其他 SQL 片段,在 applyIncludes() 方法递归的过程中,如果遇到 ${} 占位符,则使用 variablesContext 集合中的键值对进行替换;
  • 最后,将 <include> 标签替换成 <sql> 标签的内容。

通过上面逻辑可以看出,<include> 标签和 <sql> 标签是可以嵌套多层的,此时就会涉及 applyIncludes() 方法的递归,同时可以配合 “${}” 占位符,实现 SQL 片段模板化,更大程度地提高 SQL 片段的重用率。

4.2 处理 selectKey 标签

在有的数据库表设计场景中,我们会添加一个自增 ID 字段作为主键,例如:用户 ID、订单 ID 或者这个自增 ID 本身并没有什么业务含义,只是一个唯一标识而已。在某些业务逻辑里面,我们希望在执行 insert 语句的时候返回这个自增 ID 值,<selectKey> 标签就可以实现自增 ID 的获取。<selectKey> 标签不仅可以获取自增 ID,还可以指定其他 SQL 语句,从其他表或执行数据库的函数获取字段值。

parseSelectKeyNode() 方法是解析 <selectKey> 标签的核心所在,其中会解析 <selectKey> 标签的各个属性,并根据这些属性值将其中的 SQL 语句解析成 MappedStatement 对象。

4.3 处理 SQL 语句

经过 <include> 标签和 <selectKey> 标签的处理流程之后,XMLStatementBuilder 中的 parseStatementNode() 方法接下来就要开始处理 SQL 语句了,相关的代码片段如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void parseStatementNode() {
  // 解析 <selectKey> 和 <include> 标签的逻辑,省略
  // 当执行到这里的时候,<selectKey> 和 <include> 标签已经被解析完毕,并删除掉了
  // 通过 LanguageDriver.createSqlSource() 方法创建 SqlSource 对象
	SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  // 获取 SQL 标签中配置的 resultSets、keyProperty、keyColumn 等属性,
  // 以及前面解析 <selectKey> 标签得到的 KeyGenerator 对象等
	StatementType statementType = StatementType.valueOf(
    context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
	Integer fetchSize = context.getIntAttribute("fetchSize");
	Integer timeout = context.getIntAttribute("timeout");
	String parameterMap = context.getStringAttribute("parameterMap");
	String resultType = context.getStringAttribute("resultType");
	//...
  // 根据上述属性信息创建 MappedStatement 对象,并添加到 Configuration.mappedStatements 集合中保存
	builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
}

这里解析 SQL 语句使用的是 LanguageDriver 接口,其核心实现是 XMLLanguageDriver,继承关系如下图所示:

1
2
3
4
5
6
7
8
  +----------------+
  | LanguageDriver |
  +----------------+
          ^
          |
+---------+----------+
| XMLLanguageDriver  |
+--------------------+

createSqlSource() 方法中,XMLLanguageDriver 会依赖 XMLScriptBuilder 创建 SqlSource 对象,XMLScriptBuilder 首先会判断 SQL 语句是否为动态 SQL,判断的核心逻辑在 parseDynamicTags() 方法中,核心实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
protected MixedSqlNode parseDynamicTags(XNode node) {
  List<SqlNode> contents = new ArrayList<>();
  // 解析后的 SqlNode 结果集合
  NodeList children = node.getNode().getChildNodes();
  // 获取 SQL 标签下的所有节点,包括标签节点和文本节点
  for (int i = 0; i < children.getLength(); i++) {
    XNode child = node.newXNode(children.item(i));
    if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE 
        || child.getNode().getNodeType() == Node.TEXT_NODE) {
      // 处理文本节点,也就是 SQL 语句
      String data = child.getStringBody("");
      TextSqlNode textSqlNode = new TextSqlNode(data);
      // 解析 SQL 语句,如果含有未解析的 "${}" 占位符,则为动态 SQL
      if (textSqlNode.isDynamic()) {
        contents.add(textSqlNode);
        isDynamic = true;
        // 标记为动态 SQL 语句
      } else {
        contents.add(new StaticTextSqlNode(data));
      }
    } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
      // 如果解析到一个子标签,那么一定是动态 SQL
      // 这里会根据不同的标签,获取不同的 NodeHandler,然后由 NodeHandler 进行后续解析
      String nodeName = child.getNode().getNodeName();
      NodeHandler handler = nodeHandlerMap.get(nodeName);
      if (handler == null) {
        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
      }
      // 处理动态 SQL 语句,并将解析得到的 SqlNode 对象记录到 contents 集合中
      handler.handleNode(child, contents);
      isDynamic = true;
    }
  }
  // 解析后的 SqlNode 集合将会被封装成 MixedSqlNode 返回
  return new MixedSqlNode(contents);
}

这里使用 SqlNode 接口来表示一条 SQL 语句的不同部分,其中,TextSqlNode 表示的是 SQL 语句的文本(可能包含 “${}” 占位符),StaticTextSqlNode 表示的是不包含占位符的 SQL 语句文本。

另外一个新接口是 NodeHandler,它有很多实现类,如下图所示:

NodeHandler 继承关系图

NodeHandler 接口负责解析动态 SQL 内的标签,生成相应的 SqlNode 对象,通过 NodeHandler 实现类的名称,我们就可以大概猜测到其解析的标签名称。以 IfHandler 为例,它解析的就是 <if> 标签,其核心实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private class IfHandler implements NodeHandler {
  @Override
  public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    // 通过 parseDynamicTags() 方法,解析 <if> 标签下嵌套的动态 SQL
    MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
    // 获取 <if> 标签判断分支的条件
    String test = nodeToHandle.getStringAttribute("test");
    // 创建 IfNode 对象(也是 SqlNode 接口的实现),并将其保存下来
    IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
    targetContents.add(ifSqlNode);
  }
}

完成了对 SQL 语句的解析,得到了相应的 MixedSqlNode 对象之后,XMLScriptBuilder 会根据 SQL 语句的类型生成不同的 SqlSource 实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public SqlSource parseScriptNode() {
  // 对 SQL 语句进行解析
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  // 根据该 SQL 是否为动态 SQL,创建不同的 SqlSource 实现
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}


欢迎关注我的公众号,第一时间获取文章更新:

微信公众号

相关内容