在上文中我们完成了 XmlConfigBuilder
对象的构建工作,准备好了解析 XML
文件的基础环境。
所以接下来就是调用 XmlConfigBuilder
暴露的 parse()
方法来完成mybatis配置文件的解析工作了。
public Configuration parse() { if (parsed) { // 第二次调用XMLConfigBuilder throw new BuilderException("Each XMLConfigBuilder can only be used once."); } // 重置XMLConfigBuilder的解析标志,防止重复解析 parsed = true; // 此处开始进行Mybatis配置文件的解析流程 // 解析 configuration 配置文件,读取【configuration】节点下的内容 parseConfiguration(parser.evalNode("/configuration")); // 返回Mybatis的配置实例 return configuration; } 复制代码
在没有解析过的前提下,mybatis会调用 parseConfiguration(XNode root)
方法来完成 Configuration
对象的构建操作。
parseConfiguration(XNode root)
方法的入参是一个 XNode
类型的对象实例,该对象的产生是通过调用我们上文创建的 XPathParser
的 XNode evalNode(String expression)
方法来完成的。
parseConfiguration(parser.evalNode("/configuration")); 复制代码
evalNode
方法接收的是一个XPath地址表达式,字符串 "/configuration"
中的 /
表示从根节点获取元素,所以 "/configuration"
则表示获取配置文件的根元素 configuration
.
configuration
是Mybaits主配置文件的根节点,我们通常这样使用: <?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> ... </configuration> 复制代码
/** * 根据表达式解析Document对象获取对应的节点 * * @param expression Xpath地址表达式 * @return XNode */ public XNode evalNode(String expression) { // 从document对象解析出指定的节点 return evalNode(document, expression); } 复制代码
在 evalNode
方法中,将上文获取到的 XPathParser
的类属性 document
作为参数,传递给他的重载方法:
/** * 根据表达式获取节点 * @param root 根节点 * @param expression xpath表达式 * @return XNode */ public XNode evalNode(Object root, String expression) { // 获取DOM节点 Node node = (Node) evaluate(expression, root, XPathConstants.NODE); if (node == null) { return null; } // 包装成XNode节点 return new XNode(this, node, variables); } 复制代码
在重载的 evalNode
方法内部,将获取表达式对应的DOM节点的工作委托给了 evaluate
方法来完成,如果解析出了对应的DOM节点,将会以 XPathParser
对象本身和解析出来的DOM节点对象,以及用户传入的变量作为参数构造出一个 XNode
对象实例返回给方法的调用方。
被委托的 evaluate
方法利用 XPath
解析器来完成将表达式解析成指定对象的工作。
private Object evaluate(String expression, Object root, QName returnType) { try { // 在指定的上下文中计算XPath表达式,并将结果作为指定的类型返回。 return xpath.evaluate(expression, root, returnType); } catch (Exception e) { throw new BuilderException("Error evaluating XPath. Cause: " + e, e); } } 复制代码
在 XNode
类中定义了六个常量,这六个常量的初始化赋值操作都是在 XNode
节点的构造方法中完成的。
/** * XNode * * @param xpathParser XPath解析器 * @param node 被包装的节点 * @param variables 用户传入的变量 */ public XNode(XPathParser xpathParser, Node node, Properties variables) { // 初始化节点对应的解析器 this.xpathParser = xpathParser; // 初始化DOM 节点 this.node = node; // 初始化节点名称 this.name = node.getNodeName(); // 初始化用户定义的变量 this.variables = variables; // 解析节点中的属性配置 this.attributes = parseAttributes(node); // 解析节点包含的内容 this.body = parseBody(node); } 复制代码
其中 attributes
和 body
属性的取值操作需要分别通过 parseAttributes
和 parseBody
方法来完成。
/** * 解析节点中的属性值 * * @param n 节点 * @return 属性集合 */ private Properties parseAttributes(Node n) { // 定义 Properties对象 Properties attributes = new Properties(); // 获取属性节点 NamedNodeMap attributeNodes = n.getAttributes(); if (attributeNodes != null) { for (int i = 0; i < attributeNodes.getLength(); i++) { Node attribute = attributeNodes.item(i); // 针对每个属性的值进行一次占位符解析替换的操作 String value = PropertyParser.parse(attribute.getNodeValue(), variables); // 保存 attributes.put(attribute.getNodeName(), value); } } return attributes; } /** * 解析节点中的内容 * * @param node 节点 * @return 节点中内容 */ private String parseBody(Node node) { String data = getBodyData(node); if (data == null) { NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); data = getBodyData(child); if (data != null) { break; } } } return data; } /** * 获取CDATA节点和TEXT节点中的内容 * * @param child 节点 */ private String getBodyData(Node child) { if (child.getNodeType() == Node.CDATA_SECTION_NODE || child.getNodeType() == Node.TEXT_NODE) { // 获取CDATA节点和TEXT节点中的内容 String data = ((CharacterData) child).getData(); // 执行占位符解析操作 data = PropertyParser.parse(data, variables); return data; } return null; } 复制代码
这两个方法比较简单,唯一需要注意的就是在处理属性值和body内容的时候,调用了 PropertyParser.parse(String string, Properties variables)
方法对属性值和body体中的占位符进行了替换操作。
关于PropertyParser
PropertyParser在mybatis中担任着一个替换变量占位符的角色。主要作用就是将 ${变量名}
类型的占位符替换成对应的实际值。
PropertyParser
只对外暴露了一个 String parse(String string, Properties variables)
方法,该方法的作用是替换指定的占位符为变量上下文中对应的值,该方法有两个入参:一个是 String
类型的可能包含了占位符的文本内容,一个是 Properties
类型的变量上下文。
/** * 替换占位符 * @param string 文本内容 * @param variables 变量上下文 */ public static String parse(String string, Properties variables) { // 占位符变量处理器 VariableTokenHandler handler = new VariableTokenHandler(variables); // 占位符解析器 GenericTokenParser parser = new GenericTokenParser("${", "}", handler); // 返回闭合标签内的内容 return parser.parse(string); } 复制代码
在 parse
方法中涉及到了两个类, VariableTokenHandler
和 GenericTokenParser
.
VariableTokenHandler
是 TokenHandler
接口的一个实现类, TokenHandler
定义了一个 String handleToken(String content);
方法,该方法主要用来对客户端传入的内容进行一些额外的处理。
TokenHandler
也是策略模式的一种体现,它定义了文本统一处理的接口,其子类负责提供不同的处理策略。
具体到 VariableTokenHandler
中,该方法的作用就是替换传入的文本内容中的占位符。
VariableTokenHandler
的构造方法需要一个 Properties
类型的 variables
参数,该参数中定义的变量将用于替换占位符。
VariableTokenHandler
的占位符解析操作允许用户以 ${key:defaultValue}
的形式为指定的 key
提供默认值,即如果变量上下文中没有匹配 key
的变量值,则以 defaultValue
作为 key
的值。
占位符中取默认值时使用分隔符默认是 :
,如果需要修改,可以通过在 variables
参数中添加 org.apache.ibatis.parsing.PropertyParser.default-value-separator="自定义分隔符"
进行配置。
在占位符中使用默认值的操作默认是关闭的,如果需要开启,可以在 variables
参数中添加 org.apache.ibatis.parsing.PropertyParser.enable-default-value=true
进行配置。
下面是 VariableTokenHandler
的构造方法:
private VariableTokenHandler(Properties variables) { this.variables = variables; // 是否允许使用默认值比如${key:aaaa} this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE)); // 默认值分隔符 this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR); } 复制代码
GenericTokenParser
是一个通用的占位符解析器,他的构造方法有三个入参,分别是占位符的开始标签,结束标签,以及针对占位符内容的处理策略对象。
/** * GenericTokenParser * @param openToken 开始标签 * @param closeToken 结束标签 * @param handler 内容处理器 */ public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { this.openToken = openToken; this.closeToken = closeToken; this.handler = handler; } 复制代码
GenericTokenParser
对外提供了一个 parse(String text)
方法,该方法将会寻找匹配占位符的内容并调用 TokenHandler
对其进行处理,如果未匹配到占位符对应的内容,则返回始原内容。
在完成 XNode
对象的创建工作之后,就可以使用该对象调用 parseConfiguration(XNode root)
方法来进行真正的配置文件解析操作了:
parseConfiguration(parser.evalNode("/configuration")); 复制代码
在解析配置文件之前,我们先简单了解一下mybatis全局配置文件的DTD定义:
<!ELEMENT configuration ( properties? , settings? , typeAliases? , typeHandlers? , objectFactory? , objectWrapperFactory? , reflectorFactory? , plugins? , environments? , databaseIdProvider? , mappers? )> 复制代码
参考上面的DTD文件,我们可以发现 configuration
节点下允许出现11种类型的子节点,这些节点都是可选的,这就意味着Mybatis的全局配置文件可以不配置任何子节点(参考单元测试: org.apache.ibatis.builder.XmlConfigBuilderTest#shouldSuccessfullyLoadMinimalXMLConfigFile
)。
回头继续看方法 parseConfiguration
,在该方法中,对应着 configuration
的子节点,解析配置的工作被拆分成了多个子方法来完成:
/** * 解析Configuration节点 */ private void parseConfiguration(XNode root) { try { //issue #117 read properties first // 加载资源配置文件,并覆盖对应的属性[properties节点] propertiesElement(root.evalNode("properties")); // 将settings标签内的内容转换为Property,并校验。 Properties settings = settingsAsProperties(root.evalNode("settings")); // 根据settings的配置确定访问资源文件的方式 loadCustomVfs(settings); // 根据settings的配置确定日志处理的方式 loadCustomLogImpl(settings); // 别名解析 typeAliasesElement(root.evalNode("typeAliases")); // 插件配置 pluginElement(root.evalNode("plugins")); // 配置对象创建工厂 objectFactoryElement(root.evalNode("objectFactory")); // 配置对象包装工厂 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 配置反射工厂 reflectorFactoryElement(root.evalNode("reflectorFactory")); // 通过settings配置初始化全局配置 settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 // 加载多环境源配置,寻找当前环境(默认为default)对应的事务管理器和数据源 environmentsElement(root.evalNode("environments")); // 数据库类型标志创建类,Mybatis会加载不带databaseId以及当前数据库的databaseId属性的所有语句,有databaseId的 // 语句优先级大于没有databaseId的语句 databaseIdProviderElement(root.evalNode("databaseIdProvider")); // 注册类型转换器 typeHandlerElement(root.evalNode("typeHandlers")); // !!注册解析Dao对应的MapperXml文件 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } 复制代码
在上面代码中多次出现了 root.evalNode(String)
方法,该方法的作用是:根据传入的表达式,获取到对应的 XNode
节点对象。
public XNode evalNode(String expression) { return xpathParser.evalNode(node, expression); } 复制代码
具体的实现实际上是委托给了 xpathParser
解析器的 XNode evalNode(Object root, String expression)
方法来完成。
/** * 根据表达式获取节点 * @param root 根节点 * @param expression xpath表达式 * @return XNode */ public XNode evalNode(Object root, String expression) { // 获取DOM节点 Node node = (Node) evaluate(expression, root, XPathConstants.NODE); if (node == null) { return null; } // 包装成XNode节点 return new XNode(this, node, variables); } 复制代码
该方法我们在上文中已经看过了,这里不再赘述,现在以 propertiesElement(root.evalNode("properties"));
为例,解释一下 xpath
表达式 "properties"
的作用:
该表达式表示获取 properties
元素及其所有子元素。