Mybatis源码学习(一):配置文件的加载

概述

我们知道,一个框架的运行,往往都从配置文件的加载开始,本篇文章作为 Mybatis 源码学习的第一篇文章,将结合源码,阐述 Mybatis 是如何加载 mybatis-config.xml 配置文件的。

Mybatis 版本:3.5.8

JDK 版本:1.8

Mysql 版本:5.6.51

一、输入流的读取

我们根据源码给的测试用例,可以知道 Mybatis 可以通过以下代码获取 Xml 配置文件并转为配置类:

1
2
3
4
5
String resource = "org/apache/ibatis/builder/MinimalMapperConfig.xml";
try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLConfigBuilder builder = new XMLConfigBuilder(inputStream);
Configuration config = builder.parse();
}

拿到全局配置类 Configuration 以后就可以通过SqlSessionFactoryBuilder().build()方法去创建SqlSessionFactory了。当然,SqlSessionFactoryBuilder().build()有多个重载方法,但是无外乎都要经过转换为 Configuration 这一步。

整个过程就分为三步:

  1. 获取配置文件输入流:getResourceAsStream()
  2. 解析得到配置文件:XMLConfigBuilder
  3. 将解析得到的结果转为配置类:XMLConfigBuilder.parse()

1.程序入口

点进Resources.getResourceAsStream(resource),发现它是一个重载方法,原本的方法是需要传入 ClassLoader 的,但是这里直接传了个 null:

1
2
3
4
5
6
7
8
9
10
11
12
public static InputStream getResourceAsStream(String resource) throws IOException {
return getResourceAsStream(null, resource);
}

// 被重载的方法
public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
if (in == null) {
throw new IOException("Could not find resource " + resource);
}
return in;
}

这个方法实际依赖于classLoaderWrapper.getResourceAsStream(resource, loader)方法,而classLoaderWrapper是一个 Resources 中的一个使用饿汉式单例化的成员变量:

1
private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();

在整个Resources类中,几乎绝大部分的输入流都通过 ClassLoaderWrapper的方法进行读取。

2.ClassLoaderWrapper

ClassLoaderWrapper是一个类加载器的包装器,包含了多个类加载器,当我们使用时,它会按顺序检查类加载器是否为目标加载器,我们可以通过它去加载资源而不必考虑加载器的正确性问题。

image-20210602102537931

他的成员变量主要有两个:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 默认类加载器
ClassLoader defaultClassLoader;
// 系统加载器
ClassLoader systemClassLoader;

ClassLoaderWrapper() {
try {
// 系统加载器在类包装类创建时直接获取
systemClassLoader = ClassLoader.getSystemClassLoader();
} catch (SecurityException ignored) {
// AccessControlException on Google App Engine
}
}

其中系统加载器在类包装类创建时直接获取,而默认加载器由外部类调用时去设置。

他主要提供了三种获取资源的方法:

  • 获取 url:getResourceAsURL()
  • 获取输入流:getResourceAsStream()
  • 获取类:classForName()

3.获取输入流

我们以getResourceAsStream(String resource, ClassLoader classLoader)为例,看看他是如何实现的:

1
2
3
4
5
public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
// 先通过 getClassLoaders(classLoader) 获取类加载器
// 再通过 getResourceAsStream(resource, classLoader) 获取输入流
return getResourceAsStream(resource, getClassLoaders(classLoader));
}

其中,getClassLoaders(classLoader)方法主要用于获取按顺序获取一组类加载器,同时会把参数传入的自定义类加载器类加载器插到最前面:

1
2
3
4
5
6
7
8
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
return new ClassLoader[]{
classLoader, // 自定义类加载器
defaultClassLoader, // 默认类加载器
Thread.currentThread().getContextClassLoader(), // 线程上下文类加载器
getClass().getClassLoader(), // ClassLoaderWrapper的类加载器
systemClassLoader};// 系统加载器
}

接着,拿到一组 ClassLoader 以后,再去执行 getResourceAsStream(String resource, ClassLoader[] classLoader)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
// 遍历类加载器,执行非空加载器
for (ClassLoader cl : classLoader) {
if (null != cl) {

// 读取输入流
InputStream returnValue = cl.getResourceAsStream(resource);

// 如果拿不到,尝试改个路径再试试
if (null == returnValue) {
returnValue = cl.getResourceAsStream("/" + resource);
}

// 如果能获取到输入流,说明需要的类加载器已经找到了,直接终止循环返回输入流
if (null != returnValue) {
return returnValue;
}
}
}
return null;
}

4.总体流程

综合以上步骤,当我们调用Resources.getResourceAsStream()方法时,整个流程其实是这样的:

  • 先调用getClassLoaders()方法,获取一个类加载数组;
  • 类加载数组按顺序放入传入的自定义类加载器、默认类加载器、线程上下文类加载器、ClassLoaderWrapper的类加载器、系统加载器;
  • 遍历类加载器数组,使用类加载器根据传入路径加载资源,如果其中一个能加载到资源,就不再使用后面的加载器。
  • 返回输入流。

以上流程同样使用于classForName()或者getResourceAsURL()

也不难看出,Resouces这个类实际上就是在 ClassLoaderWrapper 的基础上对获取到的结果进行进一步处理,比如把输入流转为 Reader 或者文件再返回。

至此,我们读取到了配置文件。

二、输入流的解析

资源加载主要的包都在 org.apache.ibatis.parsing包下。

当我们获取到配置文件以后,解析并将其转为XMLConfigBuilder类,他的构造函数很多,我们以XMLConfigBuilder(InputStream inputStream)为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public XMLConfigBuilder(InputStream inputStream) {
this(inputStream, null, null);
}


public XMLConfigBuilder(Reader reader, String environment, Properties props) {
// 创建一个XPathParser
this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
}

// 全参构造函数
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
// 初始化一个具有默认参数的Configuration
super(new Configuration());

ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}

可以看到,XMLConfigBuilder创建实际上也分为两部分:

  • 初始化一个 XPathParser用于后续解析;
  • 初始一个具有基本参数的 Configuration

1.XPathParser

XPathParser类看名字,很容易知道这是一个解析类,他的构造函数还需要传入一个 XMLMapperEntityResolver

关于XMLMapperEntityResolver这个类,他实现了 java.io.EntityResolver接口,这个接口的作用如下:

对于解析一个 SAX 文件,程序首先会读取该 Xml 文档上的声明,根据声明去寻找相应的 DTD 声明,以便对文档格式的进行验证。

默认的寻找规则即通过实现上声明的 DTD 的 URI 地址来下载DTD声明,但是当相应的 DTD 没找到就会报错,所以理想情况是我们直接在本地放一个 DTD 文件,程序加载时直接去本地对应的地址加载。而 EntityResolver 的接口作用就是规定程序如何去寻找 DTD 。

当我们实现接口的 setEntityResolver() 并向 SAX 驱动器注册一个实例后,程序就会有有限根据 SAX 驱动器里面的规则进行寻址。

XMLMapperEntityResolver里面就规定了程序如何去指定位置找到 Mybatis 的 DTD 文件,比如 Mybaits 的配置文件与 Mapper 文件就在这两个常量:

1
2
private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";

接着,在构建 xml 解析器 XPathParser的时候,放入XMLMapperEntityResolver与输入流以及其他必要参数

1
XPathParser parser = new XPathParser(inputStream, true, null, new XMLMapperEntityResolver());

此时 XPathParser 实例完成初始化,输入流的 XML 文件被解析为 dom 树并存放在了一个 org.w3c.dom.Document类型的成员变量 document中,通过内部提供 evalNode()方法可以解析为节点 XNode 实例,通过对 XNode 类型的节点进一步解析,使用 evalXXX()为开口的各种方法解析处对应的数据。

关于 XPathParser,更具体的可以参考Mybatis源码学习(5)-解析器模块之XNode、XPathParser_向心行者-CSDN博客

2.XMLConfigBuilder

XMLConfigBuilder类内部提供诸如dataSourceElement()这类 XXXElement()的方法去直接获取指定的一组标签数据,通过针对配置文件的各个节点的解析,最终得到完整的配置文件数据。

整个逻辑看起来不简单,实际上对应的代码就两行:

1
2
XMLConfigBuilder builder = new XMLConfigBuilder(inputStream);
Configuration config = builder.parse();

其中 parse()方法如下:

1
2
3
4
5
6
7
8
9
public Configuration parse() {
// 转换一次以后就不允许转换第二次了
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

实际上调用的是 parseConfiguration()方法:

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
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);

// 加载自定义的别名与类的映射关系
typeAliasesElement(root.evalNode("typeAliases"));
// 加载定义拦截器插件
pluginElement(root.evalNode("plugins"));

// 加载自定义实例工厂
objectFactoryElement(root.evalNode("objectFactory"));
// 加载自定义实例包装类工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 加载自定义反射工厂
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);

// 加载环境变量
environmentsElement(root.evalNode("environments"));
// 加载数据库标识符
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 加载自定义类型和处理器
typeHandlerElement(root.evalNode("typeHandlers"));
// 加载mapepr接口
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}

这些数据最终被解析至它的内部变量 configuration中,也就是parse()获取到的对象。

总结

image-20210603100220028

当 Mybatis 获取配置文件的主要流程如下:

1.配置读取阶段

  • 通过 Resources 获取文件输入流;
  • Resources内部维护了一个 ClassLoaderWrapper 实例,通过ClassLoaderWrapper 先调用getClassLoaders()方法,获取一个类加载数组;
  • 类加载数组按顺序放入传入的自定义类加载器、默认类加载器、线程上下文类加载器、ClassLoaderWrapper的类加载器、系统加载器;
  • 遍历类加载器数组,使用类加载器根据传入路径加载资源,如果其中一个能加载到资源,就不再使用后面的加载器。
  • 返回输入流。

2.配置解析阶段

  • 通过XMLConfigBuilder去解析文件输入流;
  • XMLConfigBuilder内部维护了XPathParser作为解析器,并通过XMLMapperEntityResolver指定解析器寻找的 DTD 文件路径;
  • 解析器将 Xml 文件解析为标签树;
  • XMLConfigBuilder根据标签名,通过解析器获取相关配置,并放入configuration配置类;
  • 返回配置类。

值得一提的是,这里面提到了XPathParser这么个工具类,理论上只要自己通过 DTD 规定一下 Xml 文件格式,我们也能自己 DIY 一下配置文件读取器。

0%