MyBatis 源码学习(三):XML 配置文件解析和加载流程

656 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情

1. MyBatis 初始化流程

根据对 MyBatis 的初步了解,得到 MyBatis 的初始化流程,并用简易代码表示为

@Test
void test(){
    // 配置文件路径信息,默认为 classpath:
    String resource = ".../mybatis-config.xml";
    // 获取 mybatis-config.xml 配置文件的输入流对象
    try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
        // 传入配置文件流对象,使用 SqlSessionFactoryBuilder().build() 创建工厂函数对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 根据工厂函数方法 sqlSessionFactory.openSession() 创建连接对象
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // 在连接会话中获取指定的 Mapper 映射接口文件
            FolderMapper postMapper = session.getMapper(FolderMapper.class);
            // 调用接口中定义的 SQL 方法,并通过方法注解或 XML 映射文件解析到具体 SQL 语句执行
            List<FolderFlatTree> folders = postMapper.findWithSubFolders("Root");
            // 验证结果
            Assertions.assertEquals(3, folders.size());
        }
    }
}

对应的配置文件 mybatis-config.xml 已经在之前的文章 MyBatis 源码学习(二):XML 配置文件 中介绍过其内容结构,这次就来看一下源码中是如何对该配置文件进行解析的。

2. SqlSessionFactoryBuilder

说到解析 mybatis-config.xml 配置文件,总是少不了 SqlSessionFactoryBuilder 类的身影,因为 mybatis-config.xml 文件的解析就发生在该类的 build() 方法中。

SqlSessionFactoryBuilder 类的构造比较简单,其中仅仅定义了 build() 一个方法,但是却存在着许多不同的重载方法,即针对方法传参不同来执行不同的逻辑处理,而 XML 配置文件解析时调用了 InputStream 对象参数的方法。

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSessionFactoryBuilder .build() 方法允许调用时传入的参数包括 Reader 对象和 InputStream 对象,这是两种不同的方式来读取配置文件,Reader 代表字符流对象,InputStream 代表字节流对象。

但是无论是哪种方式读取配置文件,最终都会走到 SqlSessionFactoryBuilder.build() 方法中进行如下操作

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    // 只解析配置文件,environment 和 properties 默认为 null
    try {
        // 如果配置文件读作 Reader 类型,则使用 ①
        // XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
        // 配置文件读作 InputStream 对象
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        return build(parser.parse());
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        ErrorContext.instance().reset();
        try {
            if (inputStream != null) {
                inputStream.close();
            }
        } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
        }
    }
}

通过源码可以看到,无论是哪种方式,最终都是通过构建 XMLConfigBuilder 对象来解析 XML 文件的,通过传入的配置文件参数构建得到 XMLConfigBuilder 对象,并调用对象的 parse() 方法解析配置文件。此时可以跟随源码逻辑进入到 XMLConfigBuilder 类中。

3. XMLConfigBuilder

XMLConfigBuilder 类继承了 BaseBuilder 抽象类,主要是用来通过 XML 配置得到 Configuration 对象的,在源码中可以看到 XMLConfigBuilder 类中除了构造方法外,仅有 parse() 方法是允许外部调用的,其他方法均是私有方法。

根据 SqlSessionFactoryBuilder .build() 方法中 Reader 和 InputStream 两种不同文件参数,可以看到对应的 XMLConfigBuilder 构造方法为:

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

// InputStream
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}

根据 SqlSessionFactoryBuilder .build() 方法传递参数可知,environment 和 props 为 null,而两种构造函数最终都会调用 XMLConfigBuilder 的另外一个重载构造函数:

private XMLConfigBuilder(XPathParser parser, String environment, Properties props)

此时,读取的 mybatis-config.xml 配置文件对象传递到了 XPathParser 中,并作为初始化对象的参数。

3.1 XPathParser

XPathParser 初始化时,会将传入的 Reader 或 InputStream 对象转换成为 InputSource 对象来作为参数,并根据 InputSource 对象创建 Document 对象来存储 XML 配置文件中的内容。

至此, Reader 和 InputStream 都变成了 InputSource,即两种类型实现了殊途同归。

再说回到 XMLConfigBuilder 类,其构造方法中传入了 XPathParser 对象,则进入到以下的构造方法中

private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
}

构造方法首先初始化一个默认的 Configuration 属性对象,并将 XPathParser 对象赋值到 XMLConfigBuilder 对象的 parser 属性中。

此时 XMLConfigBuilder 对象的两个属性, configuration(Configuration 对象)和包含 XML 文件解析成的 Document 对象的 parser(XPathParser 对象)就产生了一些联系,碰头了。

3.2 parse()

XMLConfigBuilder 对象创建完成了,代码又返回到 SqlSessionFactoryBuilder.build() 方法中,接下来执行的是

// parser 即初始化完成的 XMLConfigBuilder 对象
return build(parser.parse());

还记得刚才说到 XMLConfigBuilder 类,除了构造方法外仅剩下一个可以外部调用的方法,就是 parse() 方法,来看一下该方法的逻辑实现。

public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}
  1. 其中 parsed 是一个布尔值,XMLConfigBuilder 对象初始化时设置为 false,当调用一次 parse() 方法后置为 true

  2. 之后调用 parseConfiguration() 方法,传入 parser.evalNode("/configuration") 参数

  3. 最后返回 configuration 对象

注意: 此处 parseConfiguration() 方法中又出现了一个 parser 对象,需要做好区分

  • SqlSessionFactoryBuilder.build() 中, parserXMLConfigBuilder对象,其属性中有 XPathParser 对象和 Configuration 对象

  • 而在 XMLConfigBuilder 的对象中,XPathParser 对象变量名称也叫 parser

3.3 parseConfiguration()

再看 XMLConfigBuilder 对象的 parseConfiguration() 方法,传入 parser.evalNode("/configuration") 作为参数。

parser(XPathParser 对象)中有一个 Documnet 对象是从 XML 配置文件解析而来,evalNode("/configuration") 方法就是对 Documnet 对象中的 configuration 标签内容进行获取。

configuration 其实就是 XML 配置文件的根标签,因此 parser.evalNode("/configuration") 会将 configuration 标签中的所有配置内容获取到,作为 XNode 对象。

然后看 parseConfiguration() 方法的逻辑处理

private void parseConfiguration(XNode root) {
    try {
        // issue #117 read properties first
        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);
        // read it after objectFactory and objectWrapperFactory issue #631
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        typeHandlerElement(root.evalNode("typeHandlers"));
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

方法中将 XNode 中的所有配置内容赋值到 Configuration 对象中,如

  • propertiesElement() 设置 properties 标签内容

  • settingsElement() 设置 settings 标签内容

  • databaseIdProviderElement() 设置数据库连接信息

  • environmentsElement() 设置 environments 标签内容

  • typeHandlerElement() 进行类型注册操作

  • mapperElement() 设置 mappers 标签内容,即映射文件解析

此处内容对应了 mybatis-config.xml 配置文件中的各项配置内容,可以参考文章:MyBatis 源码学习(二):XML 配置文件

最后,返回赋值完成的 Configuration 对象。

3.4 SqlSessionFactory

此时,代码逻辑再次返回到 SqlSessionFactoryBuilder .build() 方法中,不过这一次 build() 方法接收的是 Configuration 对象参数,对应了其中最后一个重载方法

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

方法中通过 DefaultSqlSessionFactory 类传入 Configuration 对象,初始化了一个默认的 SqlSessionFactory 对象,至此,千呼万唤的 qlSessionFactory 对象终于出来了!

4. Configuration

Configuration 对象是 MyBaits 中十分重要的配置对象,其中将 MyBatis 默认配置和用户自定义配置等信息写入,并在 MyBatis 整个执行过程中不断的使用,可以说是包含了 MyBatis 需要的所有信息。

今天简单的梳理一下解析、读取 XML 配置文件得到 Configuration ,并在此基础上初始化 SqlSessionFactory 对象的流程,并没有具体的介绍 Configuration 对象内容,因为在之后的代码中,还会经常遇到它、再次认识它。