一、Mybatis核心源码:配置文件解析过程分析

配置文件解析过程分析

一、Demo

1、配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="logImpl" value="SLF4J"/>
    </settings>
    <environments default="dev">
        <environment id="dev">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false&amp;useJDBCCompliantTimezoneShift=true&amp;useLegacyDatetimeCode=false&amp;serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="xml/TestMapper.xml"/>
    </mappers>
</configuration>

2、Java类

public class MyBatisDemo {
    private static SqlSessionFactory sqlSessionFactory;

    public static SqlSession getSqlSession() throws FileNotFoundException {
        // mybatis配置文件
        InputStream configFile = new FileInputStream(
                "E:\\demo\\mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configFile);
        // 加载配置文件得到SqlSessionFactory,从而得到SQLSession
        return sqlSessionFactory.openSession();
    }
    
    public static void main(String[] args) throws FileNotFoundException {
        SqlSession sqlSession = getSqlSession();
    }
    
    // 得到SqlSession后就可以crud了,此处不做demo。后面章节会详细讲解是如何解析mapper文件的,是如何与接口对应上的。
}

二、解析过程分析

1、简介

在上面Demo中,我们大致流程是:加载配置文件–》通过SQLSessionFactoryBuilder对象的build方法构建SQLSessionFactory对象–》通过SQLSessionFactory对象得到SQLSession。

那么问题来了:到底是怎么通过配置文件创建出来SQLSessionFactory对象的呢?

2、源码

首先我们从build方法入手。

public SqlSessionFactory build(InputStream inputStream) {
    // 重载,不bb
    return build(inputStream, null, null);
}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    // 创建配置文件的解析器(真正负责解析配置文件的类)
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    // 调用parse方法解析配置文件(真正负责解析配置文件的核心类),生成Configuration对象(后面篇幅会细说)。且将Configuration对象继续传递给重载方法。
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

// 根据上行传来的Configuration对象创建SQLSessionFactory对象。
public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

build方法完事了,貌似只明白了build是创建SQLSessionFactory对象的,但是具体如何解析配置文件的我们并不知道,只是核心方法parse负责解析。那么我们接下来就一起分析下parse方法。

public class XMLConfigBuilder extends BaseBuilder {
    // 负责解析配置文件的核心方法
    public Configuration parse() {
    // 确保配置文件只能被解析一次,避免多个线程或多个重复解析的情况。
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    // 标记配置文件已经解析,不能再次解析。
    parsed = true;
    /**
     * parser.evalNode("/configuration") 这里有个xpath表达式,此句话不做多分析,意思就是解析配置文件中的configuration节点。不知道是哪个configuration节点的,再去上面看看Demo的配置文件,根节点就是。
     * 解析出根节点下面的内容传递给parseConfiguration:真正负责解析配置文件的方法,私有的。
     */
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  /**
   * 真正解析配置文件的方法
   */
  private void parseConfiguration(XNode root) {
    try {
      // 解析配置文件里的properties标签配置
      propertiesElement(root.evalNode("properties"));
      // 解析配置文件里的settings标签配置,并将其转换为Properties对象。
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      // 加载vfs
      loadCustomVfs(settings);
      // 解析配置文件里的typeAliases标签配置
      typeAliasesElement(root.evalNode("typeAliases"));
      // 解析配置文件里的plugins标签配置
      pluginElement(root.evalNode("plugins"));
      // 解析配置文件里的objectFactory标签配置
      objectFactoryElement(root.evalNode("objectFactory"));
      // 解析配置文件里的objectWrapperFactory标签配置
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      // 解析配置文件里的reflectorFactory标签配置
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      // 将settings中的信息设置到Configuration对象中
      settingsElement(settings);
      // 解析配置文件里的environments标签配置
      environmentsElement(root.evalNode("environments"));
      // 解析配置文件里的databaseIdProvider标签配置,获取并设置databaseId到Configuration对象
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      // 解析配置文件里的typeHandlers标签配置
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析配置文件里的mappers标签配置
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
}

到此,一个完整的配置解析过程就完事了,每个标签的解析逻辑都封装到了对应的方法中,现在大家脑袋里应该有个印象了:XMLConfigBuilder类的parse方法负责解析配置文件的每个标签节点,并将解析出来的结果封装到Configuration对象中。然后SqlSessionFactoryBuilder类的build方法拿着解析出来的Configuration对象去创建SQLSessionFactory。下面我们将分析几个重点的解析方法

2.1、解析properties标签

2.1.1、配置
<properties resource="jdbc.properties">
    <property name="jdbc.username" value="hello"/>
    <property name="jdbc.password" value="world"/>
    <property name="helloworld" value="helloworld"/>
</properties>
jdbc.username=123
jdbc.password=456
jdbc.url=xxx
2.1.2、源码

上面配置包含了一个resource属性和三个子节点,接下来我们分析源码

public class XMLConfigBuilder extends BaseBuilder {
    private void propertiesElement(XNode context) throws Exception {
      if (context != null) {
          /**
           * 解析properties的子节点并将这些节点内容转换为属性对象Properties
           * 注意这时候Properties里包含如下属性:
           * jdbc.username=hello
           * jdbc.password=world
           * helloworld=helloworld
           * 
           * 就是解析的配置文件里的properties标签嘛,没毛病。
           */
          Properties defaults = context.getChildrenAsProperties();
          // 获取properties节点中的resource属性
          String resource = context.getStringAttribute("resource");
          // 获取properties节点中的url属性
          String url = context.getStringAttribute("url");
          // 既设置了resource又设置了url,抛异常。
          if (resource != null && url != null) {
          	  throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
          }
          
          if (resource != null) {
              /** 
               * 从文件系统中加载并解析属性文件
               * Resources.getResourceAsProperties(resource);此方法会将resource资源里的属性
               * 解析出来放到Properties对象中。
               * 然后我们defaults.putAll(properties),所以由于key一样,覆盖掉了原来的Properties
               * 所以这步骤完成后结果如下:
               * jdbc.username=123
               * jdbc.password=456
               * jdbc.url=xxx
               * helloworld=helloworld
               */
              defaults.putAll(Resources.getResourceAsProperties(resource));
          } else if (url != null) {
              // 从文url中加载并解析属性文件
              defaults.putAll(Resources.getUrlAsProperties(url));
          }
          Properties vars = configuration.getVariables();
          if (vars != null) {
              defaults.putAll(vars);
          }
          parser.setVariables(defaults);
	      // 将属性值设置到configuration中
          configuration.setVariables(defaults);
        }
    }
}

大致上明白逻辑了,很简单,获取properties标签的子节点,然后判断设置了url还是resource,分别走不同分支去解析属性配置文件(比如上面的properties文件)。最后将解析出来的属性都放到Configuration对象中。那么具体是如何解析子节点的呢?接着往下看

public class XNode {
  /**
   * 不做太多解释了,就是解析出name和value属性并set到Properties对象中
   */
  public Properties getChildrenAsProperties() {
    Properties properties = new Properties();
    for (XNode child : getChildren()) {
      // 获取name
      String name = child.getStringAttribute("name");
      // 获取value
      String value = child.getStringAttribute("value");
      if (name != null && value != null) {
        // name、value设置到Properties中
        properties.setProperty(name, value);
      }
    }
    return properties;
  }
}

问:【2.1.1、配置】最终出来的properties属性包含哪些?值是什么?

这是一个面试题,也能讲解为什么我们properties配置文件里写的属性会覆盖掉properties标签下的key-value。

答:

jdbc.username=123
jdbc.password=456
jdbc.url=xxx
helloworld=helloworld

为什么不是
jdbc.username=hello
jdbc.password=world
jdbc.url=xxx
helloworld=helloworld

自己看上面源码注释。

2.2、解析environments标签

2.2.1、配置
<environments default="dev">
    <environment id="dev">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false&amp;useJDBCCompliantTimezoneShift=true&amp;useLegacyDatetimeCode=false&amp;serverTimezone=UTC"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </dataSource>
    </environment>
</environments>
2.2.2、源码
private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    if (environment == null) {
      // 获取default属性  
      environment = context.getStringAttribute("default");
    }
    //遍历子节点   
    for (XNode child : context.getChildren()) {
      // 获取每一个子节点的id属性
      String id = child.getStringAttribute("id");
      /**
       * 检测当前environment节点的id与上面父节点的default的值是否一致,一致true,否则false
       */
      if (isSpecifiedEnvironment(id)) {
        // 解析 transactionManager节点  
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
        // 解析 dataSource节点  
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        // 创建DataSource对象
        DataSource dataSource = dsFactory.getDataSource();
        // 构建environment对象,并将事物管理和数据源设置到Configuration对象中。  
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}

三、总结

仅对properties和environments标签进行了源码讲解,而且对properties进行了及其详细的讲解,为什么没有全部讲解:

  • 1、大的思路和核心源码有了后,其他的源码你们应该自己动手debug去学习。
  • 2、篇幅导致过长。
  • 3、像核心内容:mapper解析方法:mapperElement,我会在后面详细讲解。

核心流程:

加载配置文件–》通过SQLSessionFactoryBuilder对象的build方法构建SQLSessionFactory对象。

build方法需要Configuration对象,Configuration对象通过XMLConfigBuilder的parse方法进行解析配置文件并将解析出来的内容装配到Configuration中。

四、补充

此篇幅讲解的是mybatis核心SQLSessionFactory的创建过长。后面章节我会讲解mapper文件的解析过程以及到底是怎么跟接口对应起来的。

五、广告

QQ群:458430385

微信公众号
微信公众号

发布了28 篇原创文章 · 获赞 33 · 访问量 8315

猜你喜欢

转载自blog.csdn.net/ctwctw/article/details/89643825