转载

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

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

测试程序

创建测试类

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 的构建

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

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

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

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

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

构造 XMLConfigBuilder

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

构造 XPathParser ,且传入的 entityResolverXMLMapperEntityResolver

XPathParser

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

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

该类包括五个属性:

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

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

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

简单的赋值操作。

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

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

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

初始化 configuration

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

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

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

parse

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

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

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

  • 使用 XPathParser 获取 XML configuration 节点内容
  • 解析 configuration 下的全部配置

evalNode

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

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

这个方法只会获取特定的一个节点的内容,对应的还有 evalNodes 方法,可以获取满足表达式的所有节点内容。

parseConfiguration 解析 mybatis-config.xml

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

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

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

propertiesElement

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

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

方法步骤如下:

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

settingsAsProperties

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

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

方法步骤:

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

typeAliasesElement

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

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

方法步骤为:

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

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

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

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

registerAliases 方法的步骤如下:

  • 调用 Mybatis io 包下的 ResolverUtil 类的 find 方法,借助 VFS 找到指定包下的 class 文件
  • 遍历获取到的每个类, 过滤内部类、接口以及匿名类 ,调用别名注册方法 registerAlias
Mybatis 源码解析—— 加载 mybatis-config.xml

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

pluginElement

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

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

该方法步骤如下:

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

objectFactoryElement

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

这个配置实际用的不多,主要是用来覆盖默认对象工厂的对象实例化行为,可以创建符合自己需求的对象。

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

objectWrapperFactoryElement

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

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

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

reflectorFactoryElement

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

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

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

settingsElement

settingsElementsettings 下的配置加载到 configuration

因为其中有很多与 SQL 相关的配置项,所以需要加载 SQL 连接信息之前加载到 configuration 中。

environmentsElement

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

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

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

主要的步骤为:

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

databaseIdProviderElement

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

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

该方法步骤为:

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

typeHandlerElement

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

typeHandlerElement 方法用来注册自定义 typeHandler

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

mapperElement

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

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 开始

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

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

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

进入 DataSourceElement 方法。

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

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

Properties props = context.getChildrenAsProperties();
复制代码
Mybatis 源码解析—— 加载 mybatis-config.xml

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

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

getChildren 方法调用了 getChildNodes 得到了所有子元素。之后遍历该节点的子元素,如果子元素节点类型为 ELEMENT_NODE (元素节点),则构造 XNode 类型的节点,加入返回给 getChildrenAsProperties 方法的集合中。

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

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

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

所以我们进入 parseAttributes 方法。

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

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

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

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

该方法里,构造了变量符号处理器 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 中会有变量存在默认值的情况( 在实际开发中,基本上不会开启变量默认值功能 )。

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

原文  https://juejin.im/post/5d87b8a86fb9a06b155dfaab
正文到此结束
Loading...