<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&characterEncoding=utf-8&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
dataSource>
environment>
environments>
<mappers>
<mapper resource="xml/TestMapper.xml"/>
mappers>
configuration>
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文件的,是如何与接口对应上的。
}
在上面Demo中,我们大致流程是:加载配置文件–》通过SQLSessionFactoryBuilder对象的build方法构建SQLSessionFactory对象–》通过SQLSessionFactory对象得到SQLSession。
那么问题来了:到底是怎么通过配置文件创建出来SQLSessionFactory对象的呢?
首先我们从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。下面我们将分析几个重点的解析方法
<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
上面配置包含了一个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自己看上面源码注释。
<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&characterEncoding=utf-8&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
dataSource>
environment>
environments>
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进行了及其详细的讲解,为什么没有全部讲解:
核心流程:
加载配置文件–》通过SQLSessionFactoryBuilder对象的build方法构建SQLSessionFactory对象。
build方法需要Configuration对象,Configuration对象通过XMLConfigBuilder的parse方法进行解析配置文件并将解析出来的内容装配到Configuration中。
此篇幅讲解的是mybatis核心SQLSessionFactory的创建过长。后面章节我会讲解mapper文件的解析过程以及到底是怎么跟接口对应起来的。
QQ群:458430385