一般来说,我们使用 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
属性中加载了的重复属性。
—– mybatis中文网
从 XMLConfigBuilder
开始,调用了一下几个类:
XPathParser
是对 Parser
的进一步封装,主要包含以下几个方法:
XPath
的调用,用于获取 XML
的各个节点的信息 evalInteger
:将查找的结果转换为 Integer
evalNode
:将查找的结果转换为 XlNode
${}
进行替换
XNode
算是对 Node
的再一次封装,其中包含了各种类型转换,属性获取,以及节点名称( name
),节点内容 (body)
,节点属性结合 (attribues)
等,使得解析出来的节点更加便于使用
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 中,增加了一种指定存在则不覆盖的语法,便是 :
这种写法感在
下面看看具体的处理逻辑:
@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
增加了效率
这个类就 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
大体流程便是如上所说,明白了整体流程,便可以开始参阅真正的代码。