本文详细的分析了 Mybatis 配置文件
mybatis-config.xml
的解析过程。其中包括各种属性的加载,占位符替换等重要功能。跟着本文一起分析、理解整个过程。
测试程序
创建测试类
public class BaseFlowTest {
@Test
public void baseTest() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(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>
<properties resource="classpath:jdbc.properties"/>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/BlogMapper.xml"/>
</mappers>
</configuration>
解析步骤
由于本模块主要研究 parsing 模块的作用——负责配置文件的解析,所以忽略资源文件流的获取,定位到 SqlSessionFactory
的构建
进入到 build(Inputstream)
方法,调用了 build(Inputstream,String,Properties)
方法。
该方法的功能主要分为两步:
- 构造
XMLConfigBuilder
- 调用
XMLConfigBuilder
的parse()
方法解析配置文件
构造 XMLConfigBuilder
构造 XPathParser
,且传入的 entityResolver
为 XMLMapperEntityResolver
。
XPathParser
XPathParser
是 Mybatis 对 XPath
解析器的扩展,用于解析 mybatis-config.xml
和 *Mapper.xml
等 XML 配置文件。
该类包括五个属性:
Document document
:XML 文档 DOM 对象boolean validation
:是否对文档基于 DTD 或 XSD 校验EntityResolver entityResolver
:XML 实体解析器Properties variables
:mybatis-config.xml
中properties
标签下获取的键值对集合(包括引入的配置文件)XPath xpath
:XPath
解析器
图中的 XPathParser
构造方法即为 XMLConfigBuilder
调用的构造方法,它调用了该类的一个通用赋值方法,然后调用 createDocument
方法根据 XML 文件的输入流创建一个 DOM 对象。
简单的赋值操作。
createDocument
方法的过程也比较简单,分为三步:
- 根据设置创建
DocumentBuilderFactory
对象 - 从
DocumentBuilderFactory
中创建DocumentBuilder
对象并设置属性 - 调用
XPath
解析 XML 文件
初始化 configuration
通过 XPathParser
获取了 XML DOM 对象之后,调用了该类的通用构造方法
该方法调用了父类的构造方法,但主要还是在于初始化了 Configuration
,主要是设置了 Mybatis 中默认的 TypeAlias
和 TypeHandler
,初始化了用来存储不同配置的各种容器。
parse
初始化配置之后,调用 parse
方法。
该方法首先会检查配置文件是否已经被解析过。如果没有解析过,进行以下两个步骤:
- 使用
XPathParser
获取 XMLconfiguration
节点内容 - 解析
configuration
下的全部配置
evalNode
evalNode
方法能够根据 XPath
表达式获取满足表达式的节点内容,最后会将 Node
类型的内容封装成 XNode
类型的对象,便于替换动态值。
这个方法只会获取特定的一个节点的内容,对应的还有 evalNodes
方法,可以获取满足表达式的所有节点内容。
parseConfiguration
解析 mybatis-config.xml
这个方法可以说是解析 mybatis-config.xml
中最核心的方法了,它汇总了解析 XML 中各种自定义值的方法。
下面会分析这个方法调用的一些关键方法
propertiesElement
propertiesElement
方法用来获取 mybatis-config.xml
中 <properties>
标签中配置的键值对,包括引入的配置文件中的键值对。
方法步骤如下:
- 获取
properties
子节点property
下所有键值对 - 获取
properties
标签中的url
或者resource
属性指定资源中的键值对(两个属性只能存在一个) - 将获取到的键值对集合放入
XMLConfigBuilder
类的configuration
和parser
变量中。
settingsAsProperties
settingsAsProperties
会解析 settings
标签下一些配置的值,例如常用的 mapUnderscoreToCamelCase
、useGeneratedKeys
等配置。
方法步骤:
- 获取
settings
标签下所有的键值对 - 获取
Configuration
类对应的MetaClass
(Mybatis 封装的反射工具类,包括了该类的各种元数据信息) - 通过
metaConfig
判断 Mybatis 是否支持setting
标签中的配置的 key,不支持直接抛出异常 - 返回
settings
下的键值对集合
typeAliasesElement
typeAliasesElement
方法用来获取 mybatis-config.xml
中 typeAliases
配置的 typeAlias
。
方法步骤为:
- 获取
typeAliases
的子标签 - 解析子标签
- 解析
package
标签 - 解析
typeAlias
标签
- 解析
- 为类注册别名
typeAliases
标签下有两种子标签:typeAlias
和 package
。
实际上, package
标签的解析过程会包括 typeAlias
中属性的解析过程,所以我直接分析 package
标签的 typeAlias
获取过程即可。
registerAliases
方法的步骤如下:
- 调用 Mybatis
io
包下的ResolverUtil
类的find
方法,借助VFS
找到指定包下的 class 文件 - 遍历获取到的每个类,过滤内部类、接口以及匿名类,调用别名注册方法
registerAlias
该方法会将自定义的别名设置为小写,并判断别名是否有对应值,如果没有,则注册成功。
pluginElement
pluginElement
方法会加载自定义的插件。
该方法步骤如下:
- 遍历
plugins
下plugin
节点 - 获取
plugin
节点interceptor
属性值,并根据属性值加载对应类。(此时别名已经加载完,所以该方法会先判断属性值是否为别名。若不是,则用Resources
类加载对应的类文件。 - 为
interceptor
加载设置的属性,并将interceptor
加入configuration
中。
objectFactoryElement
这个配置实际用的不多,主要是用来覆盖默认对象工厂的对象实例化行为,可以创建符合自己需求的对象。
objectFactoryElement
方法的过程和 pluginElement
过程完全一致。只不过获取的属性为 type
。
objectWrapperFactoryElement
objectWrapperFactoryElement
方法与 objectFactoryElement
一致,只不过创建的对象变成了 ObjectWrapper
,该类对对象属性操作提供了包装好的方法。
reflectorFactoryElement
reflectorFactoryElement
方法同 objectWrapperFactoryElement
一样,会创建一个关于对象元信息的类 Reflector
,该类也是封装了类元信息的一些反射方法。
settingsElement
settingsElement
将 settings
下的配置加载到 configuration
。
因为其中有很多与 SQL 相关的配置项,所以需要加载 SQL 连接信息之前加载到 configuration
中。
environmentsElement
environmentsElement
方法会解析 mybatis-config.xml
中关于数据库连接的配置。
environments
下可以存在多个 environment
标签,但是 Mybatis 只会加载 id 等于 environments
的 default
属性值的 environment
。
主要的步骤为:
- 获取默认环境 id
- 遍历
environment
,只有 id 与默认 id 相等,才进入后续流程 - 解析
transactionManager
标签获取指定事务类型的工厂,解析dataSource
标签获取指定数据源类型的工厂 - 根据上述工厂构建
Environment
放入configuration
databaseIdProviderElement
databaseIdProviderElement
方法用来解析多数据源配置。
该方法步骤为:
- 获取
databaseIdProvider
标签type
属性值对应的DatabaseIdProvider
- 获取
configuration
中的数据库连接信息 - 连接数据库,获取数据库的产品名,与
databaseIdProvider
标签下的子标签的name
属性匹配 - 将匹配上的
name
对应的value
设置为databaseId
并放入configuration
中
typeHandlerElement
typeHandlerElement
方法用来注册自定义 typeHandler
。
整个方法流程与 typeAliases
类似,只是最后存放配置的容器不同,此处不再说明。
mapperElement
mapperElement
方法用于解析 *Mapper.xml
配置文件或 *Mapper
接口。
该方法主要步骤为:
- 遍历
mappers
下每个节点 - 根据子标签类型来解析
- 如果子标签为
package
, 则扫描包下的所有接口 - 如果子标签为
mapper
,获取resource
、url
和class
中不为空的属性值,然后根据具体属性进行对应的解析
- 如果子标签为
- 使用
MapperBuilder
的parse
方法解析对应的 XML 或者接口类
Mapper 映射配置会在后续详解。
至此,mybatis-config.xml
中的配置已全部加载完成
补充
占位符加载
在解析 environments
的模块,mybatis-config.xml
中的配置是这样,用到了占位符方便动态替换数据源的连接信息。
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
在解析占位符之前,存放键值对的文件已经被加载过,键值对存放在 XMLConfigBuilder
的 variables
属性和 XPathParser
的 variables
属性中。
接下来分析这些占位符是何时以及怎样被替换的。
从 environmentsElement
开始
可以看到,该方法在获取数据源工厂时解析了 dataSource
节点,之后调用 dataSourceElement
方法处理该节点。
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
进入 DataSourceElement
方法。
这里调用了 getChildrenAsProperties
方法来解析 datasource
标签下的子标签
Properties props = context.getChildrenAsProperties();
这里通过 getChildren
方法获取了 datasource
下的所有子元素(此处,占位符已经被替换;和 JavaScript
的 DOM 一样,文本节点也会被获取)。跟踪进 getChildren
方法。
getChildren
方法调用了 getChildNodes
得到了所有子元素。之后遍历该节点的子元素,如果子元素节点类型为 ELEMENT_NODE
(元素节点),则构造 XNode
类型的节点,加入返回给 getChildrenAsProperties
方法的集合中。
此处特意构造 XNode
类型的节点而不是直接返回 Node
类型的节点 ,是因为在构造 XNode
节点的过程中,做了动态值的替换。可以看到,在调用 XNode
构造方法时,将存放资源文件中键值对的变量 variables
作为参数传递给了 XNode
。
前文中,当得到了一个元素节点时,例如 <property name="driver" value="${driver}"/>
,会调用该构造函数,在该构造函数中,会解析节点的属性和节点的内容体。占位符占用的即是节点的一个属性。
所以我们进入 parseAttributes
方法。
该方法内,会遍历节点的所有属性,调用 PropertyParser
的 parse
方法替换占位符。
终于进入到替换占位符的核心方法了。
该方法里,构造了变量符号处理器 VariableTokenHandler
,并传给给通用符号解析器 GenericTokenParser
,这里直接将变量的开始符号设置为 ${
,结束符号为 }
,与我们的占位符 ${driver}
一致。
之后调用 parse
方法替换占位符。
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// 找占位符开始标记
int start = text.indexOf(openToken);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
// 用来记录解析后的字符串
final StringBuilder builder = new StringBuilder();
// 记录占位符的字面值。假设动态值为 ${val},则 expression = val
StringBuilder expression = null;
while (start > -1) {
// 如果 openToken 前面有转义符
if (start > 0 && src[start - 1] == '\\') {
// 不解析该占位符字面值,直接获取去掉转义符后的值
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
}
// 如果 openToken 前面无转义符
else {
if (expression == null) {
expression = new StringBuilder();
} // 如果之前找到过占位符字面值,这次将它清空
else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
// 找到占位符结束标记
// 如果这个结束标记前有转义符,则结果中直接拼上去掉转义符后的字符串,重新查找 占位符结束标记
if (end > offset && src[end - 1] == '\\') {
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
}
// 找到的占位符结束标记前无转义符,则将 openToken 与 closeTokenlse 之间的字面值赋给 express,等待后续解析
expression.append(src, offset, end - offset);
break;
}
// 如果没有找到与 openToken 对应的 closeToken,则直接将全部字符串作为结果字符串返回
if (end == -1) {
builder.append(src, start, src.length - start);
offset = src.length;
} else {
// 使用特定的 TokenHandler 获取占位符字面值对应的值
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
// 当 offset 后没有 openToken 时,跳出 while 循环
start = text.indexOf(openToken, offset);
}
// 拼接 closeToken 之后的部分
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
这个方法很长,但是算法都很容易理解。而且 Mybatis 在源码的 test
包下提供了很多测试类,其中就包括 org.apache.ibatis.parsing.GenericTokenParserTest
。运行里面的单元测试来理解这一段方法很容易。
在解释方法流程之前,先详细说明该方法中的几个变量:
offset
:用来标记文本中已被解析到的位置builder
:记录已被解析的字符串express
:记录占位符中的变量
另外,如果占位符的开始符号与结束符号之前有转义符,那么该符号不会被识别成占位符。
用文字简单解释一下流程:
-
首先获取文本中占位符开始符号
${
出现的位置start
。 -
如果有获取到,判断符号前一位是不是转义符
\
2.1 如果是,则将
offset
到${
的字符串都拼接到已处理字符串builder
中,且将offset
移动到start +openToken.length()
位置。进入第 7 步。2.2 如果不是,转义符,说明此处为占位符的开始。进入第 3 步。
-
寻找结束符号
}
出现的位置end
3.1 如果找到,继续第 4 步
3.2 找不到,进入第 5 步
-
判断符号前一位是不是转义符
\
4.1 如果是,将
offset
到end
的值都放入express
中,标记为占位符中的变量,offset
移动到end + closeToken.length()
处。进入第 3 步,继续寻找结束符。4.2 如果不是,则将
start
与end
之间的作为占位符变量传入express
中。进入第 6 步。 -
找不到结束符
}
,则将未解析部分全部加入builder
中,将offset
设置为src.length
,即标记全部文本都被解析,进入第 9 步。 -
调用
VariableTokenHandler
的handleToken
方法获取占位符变量对应的值,未获取到就返回占位符原来的值。将该值拼接到builder
中,将offset
也移动至end + closeToken.length()
位置。 -
调用
start = text.indexOf(openToken, offset)
重新寻找占位符开始位置。 -
拼接最后一个占位符结束标记
}
之后的字符串。 -
返回已解析字符串
builder.toString()
。
流程中调用的 handleToken
方法很简单。类似与在 HashMap
中找一个 key
对应的值。 handleToken
中会有变量存在默认值的情况(在实际开发中,基本上不会开启变量默认值功能)。
至此,占位符替换就完成了。
总结
这篇文章分析了加载 mybatis-config.xml
的过程,涉及到的方法很多,但是方法都不复杂,而且对于一些逻辑稍微复杂的方法,Mybatis 都提供了对应的测试类,使我们更容易理解。
在加载过程中,只详细分析了 parsing
包下的方法,其它模块的作用后续会分析。