概述
我们知道,一个框架的运行,往往都从配置文件的加载开始,本篇文章作为 Mybatis 源码学习的第一篇文章,将结合源码,阐述 Mybatis 是如何加载 mybatis-config.xml 配置文件的。
Mybatis 版本:3.5.8
JDK 版本:1.8
Mysql 版本:5.6.51
一、输入流的读取
我们根据源码给的测试用例,可以知道 Mybatis 可以通过以下代码获取 Xml 配置文件并转为配置类:
1 | String resource = "org/apache/ibatis/builder/MinimalMapperConfig.xml"; |
拿到全局配置类 Configuration
以后就可以通过SqlSessionFactoryBuilder().build()
方法去创建SqlSessionFactory
了。当然,SqlSessionFactoryBuilder().build()
有多个重载方法,但是无外乎都要经过转换为 Configuration
这一步。
整个过程就分为三步:
- 获取配置文件输入流:
getResourceAsStream()
- 解析得到配置文件:
XMLConfigBuilder
- 将解析得到的结果转为配置类:
XMLConfigBuilder.parse()
1.程序入口
点进Resources.getResourceAsStream(resource)
,发现它是一个重载方法,原本的方法是需要传入 ClassLoader
的,但是这里直接传了个 null:
1 | public static InputStream getResourceAsStream(String resource) throws IOException { |
这个方法实际依赖于classLoaderWrapper.getResourceAsStream(resource, loader)
方法,而classLoaderWrapper
是一个 Resources 中的一个使用饿汉式单例化的成员变量:
1 | private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper(); |
在整个Resources
类中,几乎绝大部分的输入流都通过 ClassLoaderWrapper
的方法进行读取。
2.ClassLoaderWrapper
ClassLoaderWrapper
是一个类加载器的包装器,包含了多个类加载器,当我们使用时,它会按顺序检查类加载器是否为目标加载器,我们可以通过它去加载资源而不必考虑加载器的正确性问题。
他的成员变量主要有两个:
1 | // 默认类加载器 |
其中系统加载器在类包装类创建时直接获取,而默认加载器由外部类调用时去设置。
他主要提供了三种获取资源的方法:
- 获取 url:
getResourceAsURL()
- 获取输入流:
getResourceAsStream()
- 获取类:
classForName()
3.获取输入流
我们以getResourceAsStream(String resource, ClassLoader classLoader)
为例,看看他是如何实现的:
1 | public InputStream getResourceAsStream(String resource, ClassLoader classLoader) { |
其中,getClassLoaders(classLoader)
方法主要用于获取按顺序获取一组类加载器,同时会把参数传入的自定义类加载器类加载器插到最前面:
1 | ClassLoader[] getClassLoaders(ClassLoader classLoader) { |
接着,拿到一组 ClassLoader
以后,再去执行 getResourceAsStream(String resource, ClassLoader[] classLoader)
1 | InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) { |
4.总体流程
综合以上步骤,当我们调用Resources.getResourceAsStream()
方法时,整个流程其实是这样的:
- 先调用
getClassLoaders()
方法,获取一个类加载数组; - 类加载数组按顺序放入传入的自定义类加载器、默认类加载器、线程上下文类加载器、
ClassLoaderWrapper
的类加载器、系统加载器; - 遍历类加载器数组,使用类加载器根据传入路径加载资源,如果其中一个能加载到资源,就不再使用后面的加载器。
- 返回输入流。
以上流程同样使用于classForName()
或者getResourceAsURL()
。
也不难看出,Resouces
这个类实际上就是在 ClassLoaderWrapper
的基础上对获取到的结果进行进一步处理,比如把输入流转为 Reader 或者文件再返回。
至此,我们读取到了配置文件。
二、输入流的解析
资源加载主要的包都在 org.apache.ibatis.parsing
包下。
当我们获取到配置文件以后,解析并将其转为XMLConfigBuilder
类,他的构造函数很多,我们以XMLConfigBuilder(InputStream inputStream)
为例:
1 | public XMLConfigBuilder(InputStream inputStream) { |
可以看到,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 | private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.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 | XMLConfigBuilder builder = new XMLConfigBuilder(inputStream); |
其中 parse()
方法如下:
1 | public Configuration parse() { |
实际上调用的是 parseConfiguration()
方法:
1 | private void parseConfiguration(XNode root) { |
这些数据最终被解析至它的内部变量 configuration
中,也就是parse()
获取到的对象。
总结
当 Mybatis 获取配置文件的主要流程如下:
1.配置读取阶段
- 通过
Resources
获取文件输入流; Resources
内部维护了一个ClassLoaderWrapper
实例,通过ClassLoaderWrapper
先调用getClassLoaders()
方法,获取一个类加载数组;- 类加载数组按顺序放入传入的自定义类加载器、默认类加载器、线程上下文类加载器、
ClassLoaderWrapper
的类加载器、系统加载器; - 遍历类加载器数组,使用类加载器根据传入路径加载资源,如果其中一个能加载到资源,就不再使用后面的加载器。
- 返回输入流。
2.配置解析阶段
- 通过
XMLConfigBuilder
去解析文件输入流; XMLConfigBuilder
内部维护了XPathParser
作为解析器,并通过XMLMapperEntityResolver
指定解析器寻找的 DTD 文件路径;- 解析器将 Xml 文件解析为标签树;
XMLConfigBuilder
根据标签名,通过解析器获取相关配置,并放入configuration
配置类;- 返回配置类。
值得一提的是,这里面提到了XPathParser
这么个工具类,理论上只要自己通过 DTD 规定一下 Xml 文件格式,我们也能自己 DIY 一下配置文件读取器。