Mybatis 3.5.5的版本
今天来看一下在xml解析的最后一块内容SqlNode,上一篇SqlSource我们可以发现SqlSource是为了提供一个获取BoundSql的方法而存在的,那么实质上存储SQL信息和动态标签的地方是在SqlNode。
我们可以看到在DynamicSqlSource中有一个root的SqlNode,存储了动态类型的SQL信息,而其他两种SqlSource中没有SqlNode的属性,这是为什么呢?结合上一篇的内容我们可以轻易的回答,因为静态类型的SqlSource解析的时机是在启动时,所以不需要一直存储着SqlNode,而是直接存储SqlNode解析之后的内容,这一过程中也会生成SqlNode,而且两者产生SqlNode是在同样的方法中,即XMLScriptBuilder类中的parseScriptNode和parseDynamicTags方法中。
一、SqlNode的生成时机
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
//判断data中是否存在${}
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
我们在解析select | update | delete | insert标签时会调用到parseScriptNode这个方法,而在这个方法中会调用parseDynamicTags方法。parseScriptNode在上一篇中我们已经说过了,我们现在来看一下另一个方法parseDynamicTags。
这个方法其实非常简单,通过循环的方式来解析根标签下面的所有标签和内容,并且判断是不是动态的,只要存在动态的就一定是动态的。其中for循环中的if-else if语句就是判断Node的类型,如果是CDATASection或着TextNode类型使用TextSqlNode去处理,如果是个Element(在这里就说明是个动态标签)就调用相应的handler来处理,第二种情况出现了动态标签就一定是动态的了。而第一种还不确定,因为String中可能存在“${}”,所以需要进一步判断。
解析完所以标签后会放进这个MixedSqlNode,接下来我们看一下它的结构。
二、SqlNode的分类
SqlNode本身是一个接口,只提供一个apply方法,用以解析当前SqlNode存储的SQL信息,我们看一下源码:
public interface SqlNode {
boolean apply(DynamicContext context);
}
很简单是不是,其中传入的这个DynamicContext也不复杂,它的功能就是存储解析后的Sql文本,放到其中final的StringJoiner中,StringJoiner是1.8之后引入的java类,它就是一个工具类能够方便拼接字符串的,有兴趣的可以自己研究一下。在这里我们不展开,我们继续看一下SqlNode的实现又有那些:
我简单列出来四个实现类,其实还有更多,不过这四种应该可以代表全部的类型,我们逐一分析。
1.首先分析MixedSqlNode这个实现类。它实质上储存的就是一个SqlNode的List,(一)中的parseDynamicTags方法生成的就是一个MixedSqlNode,其中包含了下面的所有SqlNode。而且DynamicSqlSource中存储的rootSqlNode就是一个MixedSqlNode。我们再来看一下它的apply具体实现:
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
很简单,就是把自己存的这个List分别调用apply方法。
2.我们再来看一下parseDynamicTags方法中用到的TextSqlNode实现类。我们在parseDynamicTags方法中调用了TextSqlNode的isDynamic方法,其中就是用到了如上图(SqlNode关系图)所示的两个内部类对SQL语句进行判断是否是动态的。
我们也来看一下它的apply方法:
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
结合RawSqlSource中对SqlNode的解析过程部分代码如下:
//这段代码来自于SqlSourceBuilder类
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
结合两者可知,GenericTokenParser + TokenHandler就是用来处理SQL字符串的,可以进行解析和替换等操作,具体可以通过实现一个TokenHandler来做到。之后把解析过的Sql文本加到原来的DynamicContext中。
3.在parseDynamicTags方法中,我们对TextSqlSource进行了判断,如果不是动态的,我们会把数据存在一个StaticTextSqlNode,那么这个实现类实际上就是一个最简单的类,其中直接存储着String格式的SQL的文本,apply的时候直接调用append方法就可以了。
4.最后分析的类是IfSqlNode实现类,它也可以代表所以的其他的实现类,它们都是对应于动态标签建立的。每一个这样的动态标签都会使用一个相应的handler来处理节点得到,如(一)中的代码所示,那么这些handler是在XMLScriptBuilder类创建的时候就初始化的,可以看到这篇文章的3.3中initNodeHandlerMap方法。
那么IfSqlNode的apply方法十分简单且好理解,就是通过一个ExpressionEvaluator检测test条件,如果合适,就会执行IfSqlNode中存储的contents的apply方法。
private final ExpressionEvaluator evaluator;
private final String test;
private final SqlNode contents;
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
总结一下,并不是所有SqlNode标签中都直接储存着SQL文本信息,例如IfSqlNode,它中直接存的就是test的信息,而其中的SQL文本和可能会嵌套的其他标签或文本通过SqlNode的嵌套来进行存储。
至此,通过前面两篇文章和这篇文章我们已经了解了Mybatis将我们的config.xml和mapper.xml解析以及储存的主要过程,接下来会对Mybatis执行的主要过程进行研究。
前两篇文章:xml文件加载过程,SqlSource的内容