涉及到的类以及关键逻辑
XNode
mybatis 在解析节点时使用的是 w3c 提供的 XML 解析工具,其类型为 org.w3c.dom.Node
;但 mybatis 会使用 XNode
将其包装,并且在 XNode
的构造函数中就直接完成了占位符参数的解析:
public XNode(XPathParser xpathParser, Node node, Properties variables) { this.xpathParser = xpathParser; this.node = node; this.name = node.getNodeName(); this.variables = variables; this.attributes = parseAttributes(node); // 解析属性 this.body = parseBody(node); // 解析 body }
PropertyParser
`XNode
在解析时调用了内部函数 parseAttributes()
和 parseBody()
,这两个方法内部调用了静态方法 PropertyParser#parse
来解析每一个值:
/** * 解析标签对象的属性 */ private Properties parseAttributes(Node n) { Properties attributes = new Properties(); // 遍历 Node 节点的属性 NamedNodeMap attributeNodes = n.getAttributes(); if (attributeNodes != null) { for (int i = 0; i < attributeNodes.getLength(); i++) { Node attribute = attributeNodes.item(i); // 使用 PropertyParser 解析占位符值 String value = PropertyParser.parse(attribute.getNodeValue(), variables); attributes.put(attribute.getNodeName(), value); } } return attributes; }
而 PropertyParser#parse
方法内部的逻辑如下:
public static String parse(String string, Properties variables) { // 内部类实现的 tokenHandler,支持 ${key:default} 形式的解析 VariableTokenHandler handler = new VariableTokenHandler(variables); // 使用 GenericTokenParser 来解析占位符属性 GenericTokenParser parser = new GenericTokenParser("${", "}", handler); return parser.parse(string); }
PropertyParser.VariableTokenHandler
静态内部类,实现了 TokenHandler 接口,该接口用来处理 token 并返回处理后的内容。这个实现类支持 ${keyName:default}
这种形式的占位符参数解析,其中 default
是默认值,通过配置来决定是否启用默认值:
private static class VariableTokenHandler implements TokenHandler { private final Properties variables; private final boolean enableDefaultValue; private final String defaultValueSeparator; private VariableTokenHandler(Properties variables) { this.variables = variables; // 是否启用了默认值,默认为 false,该值取自 variables['org.apache.ibatis.parsing.PropertyParser.enable-default-value'] this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE)); // 默认值分隔符,默认为 ':',该值取自 variables['org.apache.ibatis.parsing.PropertyParser.default-value-separator'] this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR); } private String getPropertyValue(String key, String defaultValue) { return variables == null ? defaultValue : variables.getProperty(key, defaultValue); } @Override public String handleToken(String content) { if (variables != null) { String key = content; if (enableDefaultValue) { // ${key:default} 这样的表达式可以支持 default 默认值,如果没有指定的 key 属性则使用默认值 final int separatorIndex = content.indexOf(defaultValueSeparator); String defaultValue = null; if (separatorIndex >= 0) { // 分隔符之前即为 key key = content.substring(0, separatorIndex); // 分隔符之后即为默认值 default defaultValue = content.substring(separatorIndex + defaultValueSeparator.length()); } if (defaultValue != null) { return variables.getProperty(key, defaultValue); } } if (variables.containsKey(key)) { return variables.getProperty(key); } } // 若没有找到指定的值,则最终返回配置的原本形式 ${xxx} return "${" + content + "}"; } }
GenericTokenParser
该类实现了解析占位符表达式的具体逻辑。这个类被定义为可以自定义解析方式,而且支持一个字符串中多个占位符参数的解析,并且该类为 public
类。在项目中若有类似的解析场景需要,可以直接使用该类(但最好复制该类代码进行客制化修改,防止依赖升级带来的逻辑不一致出现,虽然概率很小)。
该类源码如下:
package org.apache.ibatis.parsing; public class GenericTokenParser { private final String openToken; private final String closeToken; private final TokenHandler handler; public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { // 可以自定义占位符型式以及具体的参数解析实现 this.openToken = openToken; this.closeToken = closeToken; this.handler = handler; } public String parse(String text) { if (text == null || text.isEmpty()) { return ""; } // search open token // 占位符属性一般是 <elem>${xxx}</elem> 或 <elem attr1="${xxx}"/> // 但值实际上可以这样:"I am MRAG from ${userCity:重庆}" 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; do { if (start > 0 && src[start - 1] == '\') { // this open token is escaped. remove the backslash and continue. // '{openToken}' 会被认为是转义,跳过 '' 并将 '{openToken}' 看做普通字面量 // 将 [offset, start-1) 区间的内容添加进 builder,即刚好读到 '' 之前;然后直接拼接 {openToken} builder.append(src, offset, start - offset - 1).append(openToken); offset = start + openToken.length(); // offset 移到 {openToken} 之后作为下一次操作起点 } else { // found open token. let's search close token. if (expression == null) { expression = new StringBuilder(); } else { expression.setLength(0); } builder.append(src, offset, start - offset); // 将 [offset, start) 区间的内容添加进 builder offset = start + openToken.length(); // 然后移动 offset int end = text.indexOf(closeToken, offset); // 然后开始找接着 offset 之后的 closeToken while (end > -1) { if ((end <= offset) || (src[end - 1] != '\')) { // closeToken 没有被转义的条件是前面没有反斜杠,或者 closeToken 跟 openToken 连着一起的,即中间没有参数 // 中间没有参数的情况,取决于 TokenHandler 如何处理;mybatis 并没有处理这种情况 // 理论上如果 properties 含有一个空字符串 key,只要有对应的键值,就可以处理 expression.append(src, offset, end - offset); // 将中间的表达式添加进 expression break; } // this close token is escaped. remove the backslash and continue. expression.append(src, offset, end - offset - 1).append(closeToken); offset = end + closeToken.length(); end = text.indexOf(closeToken, offset); } if (end == -1) { // close token was not found. // 找不到配套的 closeToken 就不做处理了,直接当字面量处理 builder.append(src, start, src.length - start); offset = src.length; } else { // 使用 tokenHandler 进行转义处理,然后添加进 builder builder.append(handler.handleToken(expression.toString())); offset = end + closeToken.length(); // offset 移位 } } // 寻找下一个 openToken;找不到就退出循环 start = text.indexOf(openToken, offset); } while (start > -1); if (offset < src.length) { // 表达式后面还有字面量则将字面量添加进 builder builder.append(src, offset, src.length - offset); } // 处理结束 return builder.toString(); } }
该类同时还被多个类使用,比如 SqlSourceBuilder
、ForEachSqlNode
、TextSqlNode
。mapper 文件中编写的 sql 使用的参数占位符、以及直接写在 @Select @Update
等注解中的 sql,自然也使用了该类来解析。
版权声明:除特别声明外,本站所有文章皆是本站原创,转载请以超链接形式注明出处!