Mybatis 源码解析—— 加载 mybatis-config.xml

本文详细的分析了 Mybatis 配置文件 mybatis-config.xml 的解析过程。其中包括各类属性的加载,占位符替换等重要功能。跟着本文一块儿分析、理解整个过程。php

测试程序

建立测试类java

public class BaseFlowTest {
  @Test
  public void baseTest() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    try (SqlSession session = sqlSessionFactory.openSession()) {
      BlogMapper mapper = session.getMapper(BlogMapper.class);
      Blog blog = mapper.selectBlog(1);
    }
  }
}
复制代码

配置文件以下:算法

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <properties resource="classpath:jdbc.properties"/>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="mapper/BlogMapper.xml"/>
  </mappers>
</configuration>
复制代码

解析步骤

因为本模块主要研究 parsing 模块的做用——负责配置文件的解析,因此忽略资源文件流的获取,定位到 SqlSessionFactory 的构建sql

SqlSessionFactory 构建

进入到 build(Inputstream) 方法,调用了 build(Inputstream,String,Properties)方法。数据库

build

该方法的功能主要分为两步:express

  • 构造 XMLConfigBuilder
  • 调用 XMLConfigBuilderparse() 方法解析配置文件

构造 XMLConfigBuilder

XMLConfigBuilder

构造 XPathParser,且传入的 entityResolverXMLMapperEntityResolverapache

XPathParser

XPathParser

XPathParser 是 Mybatis 对 XPath 解析器的扩展,用于解析 mybatis-config.xml*Mapper.xml 等 XML 配置文件。session

该类包括五个属性:mybatis

  • Document document:XML 文档 DOM 对象
  • boolean validation:是否对文档基于 DTD 或 XSD 校验
  • EntityResolver entityResolver:XML 实体解析器
  • Properties variablesmybatis-config.xmlproperties 标签下获取的键值对集合(包括引入的配置文件)
  • XPath xpathXPath 解析器

图中的 XPathParser 构造方法即为 XMLConfigBuilder 调用的构造方法,它调用了该类的一个通用赋值方法,而后调用 createDocument 方法根据 XML 文件的输入流建立一个 DOM 对象。app

commonConstructor

简单的赋值操做。

createDocument

createDocument 方法的过程也比较简单,分为三步:

  • 根据设置建立 DocumentBuilderFactory 对象
  • DocumentBuilderFactory 中建立 DocumentBuilder 对象并设置属性
  • 调用 XPath 解析 XML 文件

初始化 configuration

经过 XPathParser 获取了 XML DOM 对象以后,调用了该类的通用构造方法

XMLConfigBuilder

该方法调用了父类的构造方法,但主要仍是在于初始化了 Configuration,主要是设置了 Mybatis 中默认的 TypeAliasTypeHandler,初始化了用来存储不一样配置的各类容器。

parse

初始化配置以后,调用 parse 方法。

parse

该方法首先会检查配置文件是否已经被解析过。若是没有解析过,进行如下两个步骤:

  • 使用 XPathParser 获取 XML configuration 节点内容
  • 解析 configuration 下的所有配置
evalNode

evalNode

evalNode 方法可以根据 XPath 表达式获取知足表达式的节点内容,最后会将 Node 类型的内容封装成 XNode 类型的对象,便于替换动态值。

这个方法只会获取特定的一个节点的内容,对应的还有 evalNodes 方法,能够获取知足表达式的全部节点内容。

parseConfiguration 解析 mybatis-config.xml

这个方法能够说是解析 mybatis-config.xml 中最核心的方法了,它汇总了解析 XML 中各类自定义值的方法。

parseConfiguration

下面会分析这个方法调用的一些关键方法

propertiesElement

propertiesElement

propertiesElement 方法用来获取 mybatis-config.xml<properties> 标签中配置的键值对,包括引入的配置文件中的键值对。

方法步骤以下:

  • 获取 properties 子节点 property 下全部键值对
  • 获取 properties 标签中的 url 或者 resource 属性指定资源中的键值对(两个属性只能存在一个)
  • 将获取到的键值对集合放入 XMLConfigBuilder 类的 configurationparser 变量中。
settingsAsProperties

settingsAsProperties

settingsAsProperties 会解析 settings 标签下一些配置的值,例如经常使用的 mapUnderscoreToCamelCaseuseGeneratedKeys 等配置。

方法步骤:

  • 获取 settings 标签下全部的键值对
  • 获取 Configuration 类对应的 MetaClass (Mybatis 封装的反射工具类,包括了该类的各类元数据信息)
  • 经过 metaConfig 判断 Mybatis 是否支持 setting 标签中的配置的 key,不支持直接抛出异常
  • 返回 settings 下的键值对集合
typeAliasesElement

typeAliasesElement

typeAliasesElement 方法用来获取 mybatis-config.xmltypeAliases 配置的 typeAlias

方法步骤为:

  • 获取 typeAliases 的子标签
  • 解析子标签
    • 解析 package 标签
    • 解析 typeAlias 标签
  • 为类注册别名

typeAliases 标签下有两种子标签:typeAliaspackage

实际上, package 标签的解析过程会包括 typeAlias 中属性的解析过程,因此我直接分析 package 标签的 typeAlias 获取过程便可。

registerAliases

registerAliases 方法的步骤以下:

  • 调用 Mybatis io 包下的 ResolverUtil 类的 find 方法,借助 VFS 找到指定包下的 class 文件
  • 遍历获取到的每一个类,过滤内部类、接口以及匿名类,调用别名注册方法 registerAlias

registerAlias

该方法会将自定义的别名设置为小写,并判断别名是否有对应值,若是没有,则注册成功。

pluginElement

pluginElement

pluginElement 方法会加载自定义的插件。

该方法步骤以下:

  • 遍历 pluginsplugin 节点
  • 获取 plugin 节点 interceptor 属性值,并根据属性值加载对应类。(此时别名已经加载完,因此该方法会先判断属性值是否为别名。若不是,则用 Resources 类加载对应的类文件。
  • interceptor 加载设置的属性,并将 interceptor 加入 configuration 中。
objectFactoryElement

objectFactoryElement

这个配置实际用的很少,主要是用来覆盖默认对象工厂的对象实例化行为,能够建立符合本身需求的对象。

objectFactoryElement 方法的过程和 pluginElement 过程彻底一致。只不过获取的属性为 type

objectWrapperFactoryElement

objectWrapperFactoryElement

objectWrapperFactoryElement 方法与 objectFactoryElement 一致,只不过建立的对象变成了 ObjectWrapper,该类对对象属性操做提供了包装好的方法。

ObjectWrapper

reflectorFactoryElement

reflectorFactoryElement

reflectorFactoryElement 方法同 objectWrapperFactoryElement 同样,会建立一个关于对象元信息的类 Reflector,该类也是封装了类元信息的一些反射方法。

Reflector

settingsElement

settingsElementsettings 下的配置加载到 configuration

由于其中有不少与 SQL 相关的配置项,因此须要加载 SQL 链接信息以前加载到 configuration 中。

environmentsElement

environmentsElement

environmentsElement 方法会解析 mybatis-config.xml 中关于数据库链接的配置。

environments 下能够存在多个 environment 标签,可是 Mybatis 只会加载 id 等于 environmentsdefault 属性值的 environment

主要的步骤为:

  • 获取默认环境 id
  • 遍历 environment,只有 id 与默认 id 相等,才进入后续流程
  • 解析 transactionManager 标签获取指定事务类型的工厂,解析 dataSource 标签获取指定数据源类型的工厂
  • 根据上述工厂构建 Environment 放入 configuration
databaseIdProviderElement

databaseIdProviderElement

databaseIdProviderElement 方法用来解析多数据源配置。

该方法步骤为:

  • 获取 databaseIdProvider 标签 type 属性值对应的 DatabaseIdProvider
  • 获取 configuration 中的数据库链接信息
  • 链接数据库,获取数据库的产品名,与 databaseIdProvider 标签下的子标签的 name 属性匹配
  • 将匹配上的 name 对应的 value 设置为 databaseId 并放入 configuration
typeHandlerElement

typeHandlerElement

typeHandlerElement 方法用来注册自定义 typeHandler

整个方法流程与 typeAliases 相似,只是最后存放配置的容器不一样,此处再也不说明。

mapperElement

mapperElement

mapperElement 方法用于解析 *Mapper.xml 配置文件或 *Mapper 接口。

该方法主要步骤为:

  • 遍历 mappers 下每一个节点
  • 根据子标签类型来解析
    • 若是子标签为 package, 则扫描包下的全部接口
    • 若是子标签为 mapper,获取 resourceurlclass 中不为空的属性值,而后根据具体属性进行对应的解析
  • 使用 MapperBuilderparse 方法解析对应的 XML 或者接口类

Mapper 映射配置会在后续详解。

至此,mybatis-config.xml 中的配置已所有加载完成

补充

占位符加载

在解析 environments的模块,mybatis-config.xml 中的配置是这样,用到了占位符方便动态替换数据源的链接信息。

<dataSource type="POOLED">
    <property name="driver" value="${driver}"/>
    <property name="url" value="${url}"/>
    <property name="username" value="${username}"/>
    <property name="password" value="${password}"/>
</dataSource>
复制代码

在解析占位符以前,存放键值对的文件已经被加载过,键值对存放在 XMLConfigBuildervariables 属性和 XPathParservariables 属性中。

接下来分析这些占位符是什么时候以及怎样被替换的。

environmentsElement 开始

environmentsElement

能够看到,该方法在获取数据源工厂时解析了 dataSource 节点,以后调用 dataSourceElement 方法处理该节点。

DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
复制代码

进入 DataSourceElement 方法。

DataSourceElement

这里调用了 getChildrenAsProperties 方法来解析 datasource 标签下的子标签

Properties props = context.getChildrenAsProperties();
复制代码

getChildrenAsProperties

这里经过 getChildren 方法获取了 datasource 下的全部子元素(此处,占位符已经被替换;和 JavaScript 的 DOM 同样,文本节点也会被获取)。跟踪进 getChildren 方法。

getChildren

getChildren 方法调用了 getChildNodes 获得了全部子元素。以后遍历该节点的子元素,若是子元素节点类型为 ELEMENT_NODE(元素节点),则构造 XNode 类型的节点,加入返回给 getChildrenAsProperties 方法的集合中。

此处特地构造 XNode 类型的节点而不是直接返回 Node 类型的节点 ,是由于在构造 XNode 节点的过程当中,作了动态值的替换。能够看到,在调用 XNode 构造方法时,将存放资源文件中键值对的变量 variables 做为参数传递给了 XNode

XNode

前文中,当获得了一个元素节点时,例如 <property name="driver" value="${driver}"/>,会调用该构造函数,在该构造函数中,会解析节点的属性和节点的内容体。占位符占用的便是节点的一个属性。

因此咱们进入 parseAttributes 方法。

parseAttributes

该方法内,会遍历节点的全部属性,调用 PropertyParserparse 方法替换占位符。

终于进入到替换占位符的核心方法了。

parse

该方法里,构造了变量符号处理器 VariableTokenHandler ,并传给给通用符号解析器 GenericTokenParser,这里直接将变量的开始符号设置为 ${,结束符号为 },与咱们的占位符 ${driver} 一致。

以后调用 parse 方法替换占位符。

public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // 找占位符开始标记
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    // 用来记录解析后的字符串
    final StringBuilder builder = new StringBuilder();
    // 记录占位符的字面值。假设动态值为 ${val},则 expression = val
    StringBuilder expression = null;
    while (start > -1) {
      // 若是 openToken 前面有转义符
      if (start > 0 && src[start - 1] == '\\') {
        // 不解析该占位符字面值,直接获取去掉转义符后的值
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      }
      // 若是 openToken 前面无转义符
      else {
        if (expression == null) {
          expression = new StringBuilder();
        } // 若是以前找到过占位符字面值,此次将它清空
        else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          // 找到占位符结束标记
          // 若是这个结束标记前有转义符,则结果中直接拼上去掉转义符后的字符串,从新查找 占位符结束标记
          if (end > offset && src[end - 1] == '\\') {
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          }
          // 找到的占位符结束标记前无转义符,则将 openToken 与 closeTokenlse 之间的字面值赋给 express,等待后续解析
          expression.append(src, offset, end - offset);
          break;
        }

        // 若是没有找到与 openToken 对应的 closeToken,则直接将所有字符串做为结果字符串返回
        if (end == -1) {
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          // 使用特定的 TokenHandler 获取占位符字面值对应的值
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      // 当 offset 后没有 openToken 时,跳出 while 循环
      start = text.indexOf(openToken, offset);
    }
    // 拼接 closeToken 以后的部分
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
复制代码

这个方法很长,可是算法都很容易理解。并且 Mybatis 在源码的 test 包下提供了不少测试类,其中就包括 org.apache.ibatis.parsing.GenericTokenParserTest。运行里面的单元测试来理解这一段方法很容易。

在解释方法流程以前,先详细说明该方法中的几个变量:

  • offset:用来标记文本中已被解析到的位置
  • builder:记录已被解析的字符串
  • express:记录占位符中的变量

另外,若是占位符的开始符号与结束符号以前有转义符,那么该符号不会被识别成占位符。

用文字简单解释一下流程:

  1. 首先获取文本中占位符开始符号 ${ 出现的位置 start

  2. 若是有获取到,判断符号前一位是否是转义符 \

    2.1 若是是,则将 offset${ 的字符串都拼接到已处理字符串 builder 中,且将 offset 移动到 start +openToken.length() 位置。进入第 7 步。

    2.2 若是不是,转义符,说明此处为占位符的开始。进入第 3 步。

  3. 寻找结束符号 } 出现的位置 end

    3.1 若是找到,继续第 4 步

    3.2 找不到,进入第 5 步

  4. 判断符号前一位是否是转义符 \

    4.1 若是是,将 offsetend 的值都放入 express 中,标记为占位符中的变量,offset 移动到 end + closeToken.length() 处。进入第 3 步,继续寻找结束符。

    4.2 若是不是,则将 startend 之间的做为占位符变量传入 express 中。进入第 6 步。

  5. 找不到结束符 },则将未解析部分所有加入 builder 中,将 offset 设置为 src.length,即标记所有文本都被解析,进入第 9 步。

  6. 调用 VariableTokenHandlerhandleToken 方法获取占位符变量对应的值,未获取到就返回占位符原来的值。将该值拼接到 builder 中,将 offset 也移动至 end + closeToken.length() 位置。

  7. 调用 start = text.indexOf(openToken, offset) 从新寻找占位符开始位置。

  8. 拼接最后一个占位符结束标记 } 以后的字符串。

  9. 返回已解析字符串 builder.toString()

流程中调用的 handleToken 方法很简单。相似与在 HashMap 中找一个 key 对应的值。 handleToken 中会有变量存在默认值的状况(在实际开发中,基本上不会开启变量默认值功能)。

至此,占位符替换就完成了。

总结

这篇文章分析了加载 mybatis-config.xml 的过程,涉及到的方法不少,可是方法都不复杂,并且对于一些逻辑稍微复杂的方法,Mybatis 都提供了对应的测试类,使咱们更容易理解。

在加载过程当中,只详细分析了 parsing 包下的方法,其它模块的做用后续会分析。

相关文章
相关标签/搜索