一般来说,我们使用MyBatis
的时候,都会通过SqlSessionBuilder
来获取SessionFactory
,而通过源码我们可以发现,XML
配置文件的解析便是在这里开始的。
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);主要代码如下:
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
//finally
ErrorContext.instance().reset();
reader.close();
}
}
代码为了阅读方便,都删除了其他结构性代码,下同
可以看见,配置文件的解析是委托给XMLConfigBuilder
进行解析。
XMLConfigBuilder
需要3个参数,
reader
:配置文件的流environment
:environment
参数properties
: 额外的属性
其中,第一个参数是我们经常使用的,而第二个参数,在于MyBatis
配置文件中,用于方便不同的环境配置不同的属性,比如开发环境,正式环境等。。
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
...
<dataSource type="POOLED">
...
</environment>
<environment id="production">
<transactionManager type="MANAGED">
...
<dataSource type="JNDI">
...
</environment>
</environments>
此时便可以通过environment
参数指定不同的环境,便于开发。
而对于properties
,是MyBatis
为了方便在程序中通过程序覆盖指定的参数属性,比如通过Http
动态获取属性等。。由此也可以看出来,MyBatis
有3个地方可以指定properties
,并且这里动态指定的properties
属性是最高的:
- 首先读取在
properties
元素体中指定的属性; -
其次,读取从
properties
元素的类路径resource
或url
指定的属性,且会覆盖已经指定了的重复属性; -
最后,读取作为方法参数传递的属性,且会覆盖已经从
properties
元素体和resource
或url
属性中加载了的重复属性。
从XMLConfigBuilder
开始,调用了一下几个类:
XPathParser
XPathParser
是对Parser
的进一步封装,主要包含以下几个方法:
- 封装对
XPath
的调用,用于获取XML
的各个节点的信息 - 对查找的结果进行处理,比如:
evalInteger
:将查找的结果转换为Integer
evalNode
:将查找的结果转换为XlNode
- …
- 将变量
${}
进行替换
XNode
XNode
算是对Node
的再一次封装,其中包含了各种类型转换,属性获取,以及节点名称(name
),节点内容(body)
,节点属性结合(attribues)
等,使得解析出来的节点更加便于使用
PropertyParser
PropertyParser
算是一个工具类,主要是用来替换MyBatis
中的变量属性的,比如${name}
这个类如果是我设计,可能会直接将替换的代码写在parse
方法里面,但是MyBatis
中parse
方法如下:
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
按照名字来看,VariableTokenHandler
是用来真正处理变量的类,比如直接替换还是怎么,而GenericTokenParser
是用来解析变量的类,其构造方法包含开始符号,比如${
或者#{
,以及闭合符号}
,以及检查到符合条件的变量应该如何处理的方法。
为什么MyBatis
需要这样设计,是因为在MyBatis
中,类似的场景还有很多,比如动态SQL#{id}
,这些不同场景处理方式不同,因此MyBatis
将TokenHandler
设计成了一个接口,不同的场景实例化不同的结果即可。
要是我设计,我可能还是会编写几种不同的静态方法,不同场景下调用不同的方法,这可能就是面向过程编程的后遗症 2333.。。
这里可以继续看看PropertyParser
,在MyBatis
3.4.2 中,增加了一种指定存在则不覆盖的语法,便是:
这种写法感在
标签中写和没写没有区别,因为它优先级本来就最低。
下面看看具体的处理逻辑:
VariableTokenHandler###handleToken
@Override
public String handleToken(String content) {
if (variables != null) {
String key = content;
//读取配置文件看是否允许使用默认值、默认关闭
if (enableDefaultValue) {
final int separatorIndex = content.indexOf(defaultValueSeparator);
String defaultValue = null;
//如果有默认值符号
if (separatorIndex >= 0) {
key = content.substring(0, separatorIndex);
//获取默认值
defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
}
//返回结果
if (defaultValue != null) {
return variables.getProperty(key, defaultValue);
}
}
//如果不允许使用默认值,则直接返回结果
if (variables.containsKey(key)) {
return variables.getProperty(key);
}
}
//未找到对应的key,则直接返回
return "${" + content + "}";
}
接下来再看看GenericTokenParser
是如何查找的对应的Key
//看起来比较多
//但是其实经过了很多版本的演变,
//其中最新一版增加了对\\去除转义的效果
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
//查找openToken
int start = text.indexOf(openToken);
//没有找到则直接返回
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
//循环查找,用来处理多个,比如{first_name},{age},在同一个字段的结果
while (start > -1) {
//如果查找到了,但是前面有反斜杠
if (start > 0 && src[start - 1] == '\\') {
//将反斜杠删除,然后不处理这个openToken
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
//已经找到openToken 继而查找closeToken
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
//先将以前的存入容器中,比如select #{name},将select 存入容器中,然后处理#{name}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
//循环处理end,因为有可能出现这种情况#{name_\\}_test},而第一个\\}不是真正的closeToken
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
//找到了closeToken,但是其被反斜杠修饰,则直接删除反斜杠
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
//找到了closeToken,则将整个表达式保存起来
expression.append(src, offset, end - offset);
break;
}
}
if (end == -1) {
// 如果在上面的循环中没有找到对应的closeToken,则放弃这个openToken
builder.append(src, start, src.length - start);
offset = src.length;
} else {
//调用TokenHandler出来匹配到的表达式
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
//将没有处理完的字符保存起来
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
上面的代码非常繁琐,但是如果仔细看就会发现是因为想要增加反斜线消除转移的功能导致的,查看
MyBatis
以前的版本,相比之下有以下几个改进
- 将
SubString()
替换为了StringBuilder.append
增加了效率- 增加了对反斜线转意的功能
Configuration
这个类就MyBatis
的配置的核心,MyBatis
的所有配置都被集中放在了Configuration
类里面,包括各种开关,以及各个Mapper
,TypeHandler
等等..下面列举几个重要的属性:
variables
: 全局变量,可以通过${name}
使用defualtExecutorType
:默认执行器类型mappingRegistry
:Mapper
注册器interceptorChain
: 拦截器链,主要用于插入各种执行器typeHandlerRegistry
:类型处理注册器typeAliasRegistry
:类型别名注册器languageRegistry
:SQL
解析器注册器- …
由上面介绍的类,我们可以大概的了解MyBatis
的配置文件的工作流程。
首先通过传递给SqlSessionFactoryBuilder
3个参数:environment
,properties
以及reader
来获取配置流。
然后SqlSessionFactoryBuilder
会通过调用XMLConfigBuilder
进行构建Configuration
类。
而XMLConfiguration
则是通过调用XPathParser
获取XML
配置文件各个节点的信息,然后赋值给Configuration
对象
XPathParser
底层是通过XPath
来获取XML
具体的信息,在获取到属性的同时,会调用PropertyParser
来对获取到的信息进行变量替换,比如${name}
替换为真正的name
大体流程便是如上所说,明白了整体流程,便可以开始参阅真正的代码。