Mybatis源码之美:2.2.将mybatis全局配置文件对应的DOM转换为XNODE对象
< 返回列表时间: 2020-06-05来源:OSCHINA
将mybatis全局配置文件对应的DOM转换为XNODE对象
在上文中我们完成了 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"?--> <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 元素及其所有子元素。
关注我,一起学习更多知识
热门排行