概述
我们知道,一个框架的运行,往往都从配置文件的加载开始,本篇文章作为 Mybatis 源码学习的第一篇文章,将结合源码,阐述 Mybatis 是如何加载 mybatis-config.xml 配置文件的。
- Mybatis 版本:3.5.8
- JDK 版本:1.8
- Mysql 版本:5.6.51
1.输入流的读取
我们根据源码给的测试用例,可以知道 Mybatis 可以通过以下代码获取 Xml 配置文件并转为配置类:
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 这一步。
整个过程就分为三步:
- 获取配置文件输入流:
getResourceAsStream() - 解析得到配置文件:
XMLConfigBuilder - 将解析得到的结果转为配置类:
XMLConfigBuilder.parse()
1.1.程序入口
点进Resources.getResourceAsStream(resource),发现它是一个重载方法,原本的方法是需要传入 ClassLoader 的,但是这里直接传了个 null:
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 中的一个使用饿汉式单例化的成员变量:
private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();
在整个Resources类中,几乎绝大部分的输入流都通过 ClassLoaderWrapper的方法进行读取。
1.2.ClassLoaderWrapper
ClassLoaderWrapper是一个类加载器的包装器,包含了多个类加载器,当我们使用时,它会按顺序检查类加载器是否为目标加载器,我们可以通过它去加载资源而不必考虑加载器的正确性问题。

他的成员变量主要有两个:
// 默认类加载器
ClassLoader defaultClassLoader;
// 系统加载器
ClassLoader systemClassLoader;
ClassLoaderWrapper() {
try {
// 系统加载器在类包装类创建时直接获取
systemClassLoader = ClassLoader.getSystemClassLoader();
} catch (SecurityException ignored) {
// AccessControlException on Google App Engine
}
}
其中系统加载器在类包装类创建时直接获取,而默认加载器由外部类调用时去设置。
他主要提供了三种获取资源的方法:
- 获取 url:
getResourceAsURL() - 获取输入流:
getResourceAsStream() - 获取类:
classForName()
1.3.获取输入流
我们以getResourceAsStream(String resource, ClassLoader classLoader)为例,看看他是如何实现的:
public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
// 先通过 getClassLoaders(classLoader) 获取类加载器
// 再通过 getResourceAsStream(resource, classLoader) 获取输入流
return getResourceAsStream(resource, getClassLoaders(classLoader));
}
其中,getClassLoaders(classLoader)方法主要用于获取按顺序获取一组类加载器,同时会把参数传入的自定义类加载器类加载器插到最前面:
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
return new ClassLoader[]{
classLoader, // 自定义类加载器
defaultClassLoader, // 默认类加载器
Thread.currentThread().getContextClassLoader(), // 线程上下文类加载器
getClass().getClassLoader(), // ClassLoaderWrapper的类加载器
systemClassLoader};// 系统加载器
}
接着,拿到一组 ClassLoader 以后,再去执行 getResourceAsStream(String resource, ClassLoader[] classLoader)
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;
}
1.4.总体流程
综合以上步骤,当我们调用Resources.getResourceAsStream()方法时,整个流程其实是这样的:
- 先调用
getClassLoaders()方法,获取一个类加载数组; - 类加载数组按顺序放入传入的自定义类加载器、默认类加载器、线程上下文类加载器、
ClassLoaderWrapper的类加载器、系统加载器; - 遍历类加载器数组,使用类加载器根据传入路径加载资源,如果其中一个能加载到资源,就不再使用后面的加载器。
- 返回输入流。
以上流程同样使用于classForName()或者getResourceAsURL()。
也不难看出,Resouces这个类实际上就是在 ClassLoaderWrapper 的基础上对获取到的结果进行进一步处理,比如把输入流转为 Reader 或者文件再返回。
至此,我们读取到了配置文件。
2.输入流的解析
**资源加载主要的包都在 ****org.apache.ibatis.parsing**包下。
当我们获取到配置文件以后,解析并将其转为XMLConfigBuilder类,他的构造函数很多,我们以XMLConfigBuilder(InputStream inputStream)为例:
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
2.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 文件就在这两个常量:
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与输入流以及其他必要参数
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.2.XMLConfigBuilder
XMLConfigBuilder类内部提供诸如dataSourceElement()这类 XXXElement()的方法去直接获取指定的一组标签数据,通过针对配置文件的各个节点的解析,最终得到完整的配置文件数据。
整个逻辑看起来不简单,实际上对应的代码就两行:
XMLConfigBuilder builder = new XMLConfigBuilder(inputStream);
Configuration config = builder.parse();
其中 parse()方法如下:
public Configuration parse() {
// 转换一次以后就不允许转换第二次了
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
实际上调用的是 parseConfiguration()方法:
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()获取到的对象。
总结
当 Mybatis 获取配置文件的主要流程如下:
配置读取阶段
- 通过
Resources获取文件输入流; Resources内部维护了一个ClassLoaderWrapper实例,通过ClassLoaderWrapper先调用getClassLoaders()方法,获取一个类加载数组;- 类加载数组按顺序放入传入的自定义类加载器、默认类加载器、线程上下文类加载器、
ClassLoaderWrapper的类加载器、系统加载器; - 遍历类加载器数组,使用类加载器根据传入路径加载资源,如果其中一个能加载到资源,就不再使用后面的加载器。
- 返回输入流。
配置解析阶段
- 通过
XMLConfigBuilder去解析文件输入流; XMLConfigBuilder内部维护了XPathParser作为解析器,并通过XMLMapperEntityResolver指定解析器寻找的 DTD 文件路径;- 解析器将 Xml 文件解析为标签树;
XMLConfigBuilder根据标签名,通过解析器获取相关配置,并放入configuration配置类;- 返回配置类。
值得一提的是,这里面提到了XPathParser这么个工具类,理论上只要自己通过 DTD 规定一下 Xml 文件格式,我们也能自己 DIY 一下配置文件读取器。