上面代码创建了一个新的 Properties 对象,并将全局 Properties 添加到其中。这样做的原因是 applyIncludes 的重载方法会向 Properties 中添加新的元素,如果直接将全局 Properties 传给重载方法,会造成全局 Properties 被污染。这是个小细节,一般容易被忽视掉。其他没什么需要注意的了,我们继续往下看。
private void applyIncludes(Node source, final Properties variablesContext, boolean included) { // ⭐️ 第一个条件分支 if (source.getNodeName().equals("include")) { /* * 获取 <sql> 节点。若 refid 中包含属性占位符 ${}, * 则需先将属性占位符替换为对应的属性值 */ Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext); /* * 解析 <include> 的子节点 <property>,并将解析结果与 variablesContext 融合, * 然后返回融合后的 Properties。若 <property> 节点的 value 属性中存在占位符 ${}, * 则将占位符替换为对应的属性值 */ Properties toIncludeContext = getVariablesContext(source, variablesContext); /* * 这里是一个递归调用,用于将 <sql> 节点内容中出现的属性占位符 ${} 替换为对应的 * 属性值。这里要注意一下递归调用的参数: * * - toInclude:<sql> 节点对象 * - toIncludeContext:<include> 子节点 <property> 的解析结果与 * 全局变量融合后的结果 */ applyIncludes(toInclude, toIncludeContext, true); /* * 如果 <sql> 和 <include> 节点不在一个文档中, * 则从其他文档中将 <sql> 节点引入到 <include> 所在文档中 */ if (toInclude.getOwnerDocument() != source.getOwnerDocument()) { toInclude = source.getOwnerDocument().importNode(toInclude, true); } // 将 <include> 节点替换为 <sql> 节点 source.getParentNode().replaceChild(toInclude, source); while (toInclude.hasChildNodes()) { // 将 <sql> 中的内容插入到 <sql> 节点之前 toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude); } /* * 前面已经将 <sql> 节点的内容插入到 dom 中了, * 现在不需要 <sql> 节点了,这里将该节点从 dom 中移除 */ toInclude.getParentNode().removeChild(toInclude); // ⭐️ 第二个条件分支 } else if (source.getNodeType() == Node.ELEMENT_NODE) { if (included && !variablesContext.isEmpty()) { NamedNodeMap attributes = source.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { Node attr = attributes.item(i); // 将 source 节点属性中的占位符 ${} 替换成具体的属性值 attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext)); } } NodeList children = source.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { // 递归调用 applyIncludes(children.item(i), variablesContext, included); } // ⭐️ 第三个条件分支 } else if (included && source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) { // 将文本(text)节点中的属性占位符 ${} 替换成具体的属性值 source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext)); } }上面的代码如果从上往下读,不太容易看懂。因为上面的方法由三个条件分支,外加两个递归调用组成,代码的执行顺序并不是由上而下。要理解上面的代码,我们需要定义一些配置,并将配置带入到具体代码中,逐行进行演绎。不过,更推荐的方式是使用 IDE 进行单步调试。为了便于讲解,我把上面代码中的三个分支都用 ⭐️ 标记了出来,这个大家注意一下。好了,必要的准备工作做好了,下面开始演绎代码的执行过程。演绎所用的测试配置如下:
<mapper namespace="xyz.coolblog.dao.ArticleDao"> <sql> ${table_name} </sql> <select resultType="xyz.coolblog.dao.ArticleDO"> SELECT id, title FROM <include refid="table"> <property value="article"/> </include> WHERE id = #{id} </select> </mapper>我们先来看一下 applyIncludes 方法第一次被调用时的状态,如下:
参数值: source = <select> 节点 节点类型:ELEMENT_NODE variablesContext = [ ] // 无内容 included = false 执行流程: 1. 进入条件分支2 2. 获取 <select> 子节点列表 3. 遍历子节点列表,将子节点作为参数,进行递归调用第一次调用 applyIncludes 方法,source = <select>,代码进入条件分支2。在该分支中,首先要获取 <select> 节点的子节点列表。可获取到的子节点如下:
编号 子节点 类型 描述1 SELECT id, title FROM TEXT_NODE 文本节点
2 <include refid="table"/> ELEMENT_NODE 普通节点
3 WHERE id = #{id} TEXT_NODE 文本节点