深入理解Mybatis(一):配置文件的加载

506 阅读8分钟

概述

我们知道,一个框架的运行,往往都从配置文件的加载开始,本篇文章作为 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 这一步。

整个过程就分为三步:

  1. 获取配置文件输入流:getResourceAsStream()
  2. 解析得到配置文件:XMLConfigBuilder
  3. 将解析得到的结果转为配置类: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 一下配置文件读取器。