一般来说,我们使用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配置文件中,用于方便不同的环境配置不同的属性,比如开发环境,正式环境等。。
...
...
...
...
此时便可以通过environment参数指定不同的环境,便于开发。
而对于properties,是MyBatis为了方便在程序中通过程序覆盖指定的参数属性,比如通过Http动态获取属性等。。由此也可以看出来,MyBatis有3个地方可以指定properties,并且这里动态指定的properties属性是最高的:
首先读取在 properties 元素体中指定的属性;
其次,读取从 properties 元素的类路径 resource 或url 指定的属性,且会覆盖已经指定了的重复属性;
最后,读取作为方法参数传递的属性,且会覆盖已经从 properties 元素体和 resource 或 url 属性中加载了的重复属性。—–mybatis中文网
从XMLConfigBuilder开始,调用了一下几个类:
XPathParser
XPathParser是对Parser的进一步封装,主要包含以下几个方法:
封装对XPath的调用,用于获取XML的各个节点的信息
对查找的结果进行处理,比如:evalInteger:将查找的结果转换为IntegerevalNode:将查找的结果转换为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的配置文件的工作流程。
首先通过传递给SqlSessionFactoryBuilder3个参数:environment,properties以及reader来获取配置流。
然后SqlSessionFactoryBuilder会通过调用XMLConfigBuilder进行构建Configuration类。
而XMLConfiguration则是通过调用XPathParser获取XML配置文件各个节点的信息,然后赋值给Configuration对象
XPathParser底层是通过XPath来获取XML具体的信息,在获取到属性的同时,会调用PropertyParser来对获取到的信息进行变量替换,比如${name}替换为真正的name
大体流程便是如上所说,明白了整体流程,便可以开始参阅真正的代码。
需要了解更多可以参考这篇文章https://www.jianshu.com/p/af52bdb8106b