MyBatis源码剖析(4)XMLConfigBuilder(2)

299 阅读9分钟

上一节整体分析了XMLConfigBuilder源码,这节就来详细分析其中的细节。本节包括对配置文件中properties和environments的分析

Properties

红色框到的部分就是解析properties的入口函数,此函数通过调用xpath中的解析函数将properties标签解析为XNode。

private void propertiesElement(XNode context) throws Exception {
  if (context != null) {
    // 获取默认的配置属性
    Properties defaults = context.getChildrenAsProperties();

    // 获取resource内容
    String resource = context.getStringAttribute("resource");
    // 网络配置路径
    String url = context.getStringAttribute("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.");
    }


    // 将数据库配置文件加入到 properties 中
    if (resource != null) {
      defaults.putAll(Resources.getResourceAsProperties(resource));
    } else if (url != null) {
      defaults.putAll(Resources.getUrlAsProperties(url));
    }


    Properties vars = configuration.getVariables();

    // 此处判断为之前是否设置过properties,即通过参数指定的 properties
    if (vars != null) {
      // 把之前的配置信息加入到当前配置信息中
      defaults.putAll(vars);
    }

    // 设置回配置文件
    parser.setVariables(defaults);
    configuration.setVariables(defaults);
  }
}

第一步就是将子节点转换为properties,咱们的节点一般配置的就是下面这样的

<!-- 加载类路径下的属性文件 -->
<properties resource="db.properties"/>

这样的是没有子节点的,我们是通过resource属性指定的额外的配置文件,其实也可以在其子标签下直接指定数据库连接信息,例如

<!-- 加载类路径下的属性文件 -->
<properties>
  <property name="driver" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="jdbc:mysql://localhost:3306/mybatis_analyze"/>
  <property name="username" value="root"/>
  <property name="password" value="123"/>
</properties>

不光可以使用resource指定数据库文件的配置信息,还可以使用url指定网络路径。但是,url和resource不可以共存,也就是不可以同时指定本地路径和网络路径。

// 将数据库配置文件加入到 properties 中
if (resource != null) {
  defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
  defaults.putAll(Resources.getUrlAsProperties(url));
}

还有几点需要进行详细说明的,就是上面代码中的Resources.getResourceAsProperties(resource)Resources.getUrlAsProperties(url),逐一进到其中进行讲解。

Resources.getResourceAsProperties(resource)

/**
 * 读取文件流,然后装载到 {@link Properties} 中
 *
 * @param resource 待加载的资源
 * @return {@link Properties}
 * @throws java.io.IOException 资源没有找到
 */
public static Properties getResourceAsProperties(String resource) throws IOException {
  Properties props = new Properties();
  try (InputStream in = getResourceAsStream(resource)) {
    props.load(in);
  }
  return props;
}

这部分就是应用了Properties中的load方法,将字节流装载到Properties中,其中,getResourceAsStream的具体实现可以到juejin.cn/post/687145…

Resources.getUrlAsProperties(url)

/**
 * 将URL网络文件地址转化为 {@link Properties}
 *
 * @param urlString 待转换的URL地址
 * @return {@link Properties}
 * @throws java.io.IOException 资源不可读
 */
public static Properties getUrlAsProperties(String urlString) throws IOException {
  Properties props = new Properties();
  try (InputStream in = getUrlAsStream(urlString)) {
    props.load(in);
  }
  return props;
}

同样的,将URL地址文件转换为字节流,然后通过Properties中的load方法将字节流装入。

进入到getUrlAsStream方法内部。

/**
 * 将URL文件转换为字节流
 *
 * @param urlString 待转化的URL地址
 * @return 字节流 {@link InputStream}
 * @throws java.io.IOException 网络资源不可读
 */
public static InputStream getUrlAsStream(String urlString) throws IOException {
  URL url = new URL(urlString);
  URLConnection conn = url.openConnection();
  return conn.getInputStream();
}

具体的实现就是获取网络连接,然后获取网络返回信息。

关于配置共存

前一节指出了properties可以通过代码参数进行指定,也可以通过xml配置文件的方式指定,并且也说了二者会进行一个合并,那么从源码中,就可以看出来了

Properties vars = configuration.getVariables();

// 此处判断为之前是否设置过properties,即通过参数指定的 properties
if (vars != null) {
  // 把之前的配置信息加入到当前配置信息中
  defaults.putAll(vars);
}

首先会先将通过参数指定的properties读出来,然后判断是否为null,其次在将读出来的信息合并到新的properties中,一同写回到Configuration中。

通过源码可以看出来在最后一步还将配置信息放入到xpath中一份,这是问什么呢?埋一个小伏笔

Environments

environments节点的解析流程相对来说是比较复杂的,里面更是涉及到了一些不常用的配置信息,看完之后你绝对会哇塞!

environmentsElement(root.evalNode("environments"));

此处为解析environments节点的入口,传入的为它的根节点。进入到代码中。

/**
 * 解析 environments
 *
 * @param context environments根节点
 * @throws Exception
 */
private void environmentsElement(XNode context) throws Exception {
  if (context != null) {

    // 判断当前之前参数传入的environment是否为null
    if (environment == null) {
      // 为null则获取配置文件中指定的 environment
      environment = context.getStringAttribute("default");
    }

    // 遍历其中的每一个 environment
    for (XNode child : context.getChildren()) {
      String id = child.getStringAttribute("id");

      // 判断默认环境与当前 id 是否相等
      if (isSpecifiedEnvironment(id)) {
        // 返回事务管理器工厂
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));

        // 返回数据源
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();

        // 生成 Environment
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}

default

谈到了多环境配置数据源,怎么配置多配置源?多配置源有什么用?

怎么配置多配置源

<!-- 设置一个默认的连接环境信息 -->
<environments default="mysql_developer">
    <!-- 连接环境信息,取一个任意唯一的名字 -->
    <environment id="mysql_developer">
        <!-- mybatis使用jdbc事务管理方式 -->
        <transactionManager type="jdbc"/>
        <!-- mybatis使用连接池方式来获取连接 -->
        <dataSource type="pooled">
            <!-- 配置与数据库交互的4个必要属性 -->
            <property name="driver" value="${mysql.driver}"/>
            <property name="url" value="${mysql.url}"/>
            <property name="username" value="${mysql.username}"/>
            <property name="password" value="${mysql.password}"/>
        </dataSource>
    </environment>

    <environment id="mysql_developer_1">
        <!-- mybatis使用jdbc事务管理方式 -->
        <transactionManager type="jdbc"/>
        <!-- mybatis使用连接池方式来获取连接 -->
        <dataSource type="pooled">
            <!-- 配置与数据库交互的4个必要属性 -->
            <property name="driver" value="${mysql.driver}"/>
            <property name="url" value="${mysql.url}"/>
            <property name="username" value="${mysql.username}"/>
            <property name="password" value="${mysql.password}"/>
        </dataSource>
    </environment>
</environments>

在一个environments中配置多个environment即为多配置源,但是每次启动的时候只能使用1个配置源,其实就靠environments中的default来限制的,每一个environment中都会有一个id,然后通过default属性来指定选择哪一个数据源。

多配置源有什么用

例如在生产和开发环境中,生产环境需要一套配置源,开发环境又需要一份配置源,在两个数据源之间切换的效率明显大于修改。


代码的第一部分就是在检查之前参数是否传递过来environment,即和上述default的作用是一样的,如果参数没有进行指定,则需要读取配置文件中的default属性值。

下一步就是循环遍历其中的每一个environment,首先获取到environment的id,判断default是否和当前遍历的id相等,相等则认为使用当前配置源。

接下来就是获取environment节点内的内容。

事务管理器工厂

事务管理器工厂是用来生成事务管理器的,在MyBatis中,有两种事务管理器工厂

  • JdbcTransactionFactory
  • ManagedTransactionFactory

这两种的区别一会再进行说明,先来说一下怎么配置。

<transactionManager type="jdbc"/>

在environment内部存在上述节点,此节点的type属性就是在配置事务管理器工厂,它有两个值

  • JDBC
  • MANAGED

两种事务管理器工厂的区别:

JDBC 使用 JdbcTransactionFactory 生成的 JdbcTransaction 对象实现。它是以 JDBC 的方式对数据库的提交和回滚进行操作。

MANAGED 使用 ManagedTransactionFactory 生成的 ManagedTransaction 对象实现。它的提交和回滚方法不用任何操作,而是把事务交给容器处理。在默认情况下,它会关闭连接,然而一些容器并不希望这样,因此需要将 closeConnection 属性设置为 false 来阻止它默认的关闭行为。 此方法一般和spring联用,将事务管理交给Spring容器。

<transactionManager type="MANAGED">
    <property name="closeConnection" value="false"/>
</transactionManager>

两种事务管理器工厂都是实现了TransactionFactory接口,是一种工厂方法模式,我们也可以写一个自己的工厂,只要实现了TransactionFactory接口以及实现Transaction就可以实现自己的工厂。

JdbcTransaction

public class JdbcTransactionFactory implements TransactionFactory {

  @Override
  public Transaction newTransaction(Connection conn) {
    return new JdbcTransaction(conn);
  }

  @Override
  public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
    return new JdbcTransaction(ds, level, autoCommit);
  }
}

在JdbcTransactionFactory内部其实就是存在两个创建JdbcTransaction事务管理器的方法,关于JdbcTransaction的解析咱们后续再进行。

ManagedTransaction

public class ManagedTransactionFactory implements TransactionFactory {

  private boolean closeConnection = true;

  @Override
  public void setProperties(Properties props) {
    if (props != null) {
      String closeConnectionProperty = props.getProperty("closeConnection");
      if (closeConnectionProperty != null) {
        closeConnection = Boolean.parseBoolean(closeConnectionProperty);
      }
    }
  }

  @Override
  public Transaction newTransaction(Connection conn) {
    return new ManagedTransaction(conn, closeConnection);
  }

  @Override
  public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
    // Silently ignores autocommit and isolation level, as managed transactions are entirely
    // controlled by an external manager.  It's silently ignored so that
    // code remains portable between managed and unmanaged configurations.
    return new ManagedTransaction(ds, level, closeConnection);
  }
}

在ManagedTransactionFactory内部,可以看到它对TransactionFactory中的setProperties进行了实现,这样就可以为它去设置属性了,关于ManagedTransaction内部的解析咱们也是后续进行。

事务管理器工厂的创建

说完了两种事务管理器工厂,那么就来聊一聊事务管理器工厂的创建

// 返回事务管理器工厂
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));

进入到transactionManagerElement内部

/**
 * 事务管理器工厂设置
 * @param context 配置文件中 transactionManager
 * @return 事务管理器
 * @throws Exception transactionManager为 null,则抛出异常
 */
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type");
    Properties props = context.getChildrenAsProperties();
    TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
    factory.setProperties(props);
    return factory;
  }
  throw new BuilderException("Environment declaration requires a TransactionFactory.");
}

获取到type属性,然后再获取到transactionManager下的节点并且转化为属性(只有MANAGE时才有子节点且name只能为closeConnection,JDBC时没有子节点)。

resolveClass方法就是根据别名去反射创建对应的工厂,关于这个方法会在Configuration中进行解析。

最后就是为工厂设置属性,然后返回。

数据源的设置

DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();

dataSourceElement传入environment内的datasource属性

/**
 * 得到数据库配置
 * @param context 配置文件中 dataSource
 * @return 数据源工厂
 * @throws Exception dataSource 为 null,则抛出异常
 */
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type");
    Properties props = context.getChildrenAsProperties();
    DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
    factory.setProperties(props);
    return factory;
  }
  throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}

获取到DataSource属性后,会获取属性值type。

type

数据源具有以下三个类型

  • POOL
  • UNPOOL
  • JNDI

它们三个的区别就是第一个使用数据库连接池,而第二个不会使用数据库连接池

第三个比较特殊,程序员开发时,知道要开发访问MySQL数据库的应用,于是将一个对 MySQL JDBC 驱动程序类的引用进行了编码,并通过使用适当的 JDBC URL 连接到数据库。 这是传统的做法,也是以前非Java程序员(如Delphi、VB等)常见的做法。这种做法一般在小规模的开发过程中不会产生问题,只要程序员熟悉Java语言、了解JDBC技术和MySQL,可以很快开发出相应的应用程序。

没有JNDI的做法存在的问题:

1、数据库服务器名称MyDBServer 、用户名和口令都可能需要改变,由此引发JDBC URL需要修改;

2、数据库可能改用别的产品,如改用DB2或者Oracle,引发JDBC驱动程序包和类名需要修改;

3、随着实际使用终端的增加,原配置的连接池参数可能需要调整;

解决办法:

程序员应该不需要关心“具体的数据库后台是什么?JDBC驱动程序是什么?JDBC URL格式是什么?访问数据库的用户名和口令是什么?”等等这些问题,程序员编写的程序应该没有对 JDBC 驱动程序的引用,没有服务器名称,没有用户名称或口令 —— 甚至没有数据库池或连接管理。而是把这些问题交给J2EE容器(tomcat)来配置和管理,程序员只需要对这些配置和管理进行引用即可。

由此,就有了JNDI。

有三种类型就会对应三个DataSourceFactory

  • JndiDataSourceFactory
  • PoolDataSourceFactory
  • UnPoolDataSourceFactory

这三种数据源工厂咱们下节和TransactionFactory一起分析。

最后通过别名反射创建数据源工厂,并且设置回属性,即连接信息。

回到environmentsElement方法中。

下一步就是创建Environment对象。

// 生成 Environment
Environment.Builder environmentBuilder = new Environment.Builder(id)
    .transactionFactory(txFactory)
    .dataSource(dataSource);

Environment.Builder是environment的一个内置对象,通过内部的build()方法创建Environment对象。

最后将Environment设置回Configuration中。


这一节稍微有点长,涉及的细节也是比较多的,希望各位耐心琢磨。

咱们下一节就对TransactionFactory和DataSourceFactory源码进行分析。

看源码一时爽,一直看一时爽。