上一节整体分析了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源码进行分析。
看源码一时爽,一直看一时爽。