Spring mybatis源码篇章-动态SQL节点源码深入
通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-动态SQL基础语法以及原理
前话
前文描述到通过mybatis默认的解析驱动类org.apache.ibatis.scripting.xmltags.XMLLanguageDriver,将mapper文件的CRUD节点均解析成对应的SqlNode接口。
本文将在前文的基础上具体分析select|update|insert|delete节点内的其他节点是如何被解析的,例如trim/where/set等嵌套节点,算是深入下mybatis的源码
SqlNode
优先观察下公共接口类org.apache.ibatis.scripting.xmltags.SqlNode,代码如下
public interface SqlNode {
boolean apply(DynamicContext context);
}
内部只有一个apply()方法,其应该会在上下文context操刀,我们先可以看下有哪些类型的SqlNode
TextSqlNode
对类型为CDATA块或者TEXT的包装为TextSqlNode对象,形如
<select id="testQuery">
<![CDATA[
select * from tb_test
]]>
</select>
或者
<select id="testQuery" parameterType="java.lang.String">
select * from tb_test where name = #{name}
</select>
笔者此处看下其是如何实现apply()方法把,代码如下
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context));
context.appendSql(parser.parse(text));
return true;
}
关于如何解析相关的SQL语句就不展开了,大意上是针对含有${}
符号的字符串进行相应的替换,从而拼装成完整的SQL保存至相应的org.apache.ibatis.scripting.xmltags.DynamicContext上下文对象中
另外此类还有一个关键的方法isDynamic(),其是为了判断相应的字符串是否为动态SQL
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
// same as
GenericTokenParser parser = createParser(checker);
parser.parse(text);
// true while the text contains '${}'
return checker.isDynamic();
}
只有SQL语句含有${}
标志符号,才会返回true
StaticTextSqlNode
保存无特殊字符${}
的SQL语句,即其是建立在上述的TextSqlNode#isDynamic()方法的基础上,在返回为false的情况下被包装。
其apply()方法也特别的简单
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
MixedSqlNode
其内部只有一个类型为java.util.List的contents属性,主要作用是保存一个CRUD节点下所含有的所有SqlNode集合,方便统一管理以及解析
public class MixedSqlNode implements SqlNode {
private List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
}
其他类型的SqlNode基本都是基于MixedSqlNode来进行包装解析的,而它们基本都有对应的NodeHandler类来与之对应解析
NodeHandler
其是org.apache.ibatis.scripting.xmltags.XMLScriptBuilder的私有内部接口类,而相应的动态SQL节点都是提前被保存在了下述代码中的nodeHandlers变量中
private Map<String, NodeHandler> nodeHandlers = new HashMap<String, NodeHandler>() {
private static final long serialVersionUID = 7123056019193266281L;
{
put("trim", new TrimHandler());
put("where", new WhereHandler());
put("set", new SetHandler());
put("foreach", new ForEachHandler());
put("if", new IfHandler());
put("choose", new ChooseHandler());
put("when", new IfHandler());
put("otherwise", new OtherwiseHandler());
put("bind", new BindHandler());
}
};
BindHandler
用于解析bind标签节点
例如
<bind name="title" value="'%' + _parameter.getTitle() + '%'"/>
对应的解析语句如下
private class BindHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//获取name和value属性
final String name = nodeToHandle.getStringAttribute("name");
final String expression = nodeToHandle.getStringAttribute("value");
//包装成简单的VarDeclSqlNode类
final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
targetContents.add(node);
}
}
会包装成org.apache.ibatis.scripting.xmltags.VarDeclSqlNode节点进行相应的解析
VarDeclSqlNode#apply()
public boolean apply(DynamicContext context) {
final Object value = OgnlCache.getValue(expression, context.getBindings());
context.bind(name, value);
return true;
}
很明显就是将name和计算后的真实value值对应关系保存至DynamicContext#bindings属性(HashMap类型)中
TrimHandler
用于解析trim标签节点
private class TrimHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//trim标签下可包含where/set/if/when等标签,将之封装成MixedSqlNode
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
// read prefix/preffixOverrides/suffix/suffixOverrides properties
String prefix = nodeToHandle.getStringAttribute("prefix");
String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
String suffix = nodeToHandle.getStringAttribute("suffix");
String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
// delegate TrimSqlNode to process trim sql
TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
}
直接包装成org.apache.ibatis.scripting.xmltags.TrimSqlNode对象
TrimSqlNode
首先观察下构造函数
public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
}
protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
//这里contents一般为MixedSqlNode,内部包含多个SqlNode
this.contents = contents;
this.prefix = prefix;
this.prefixesToOverride = prefixesToOverride;
this.suffix = suffix;
this.suffixesToOverride = suffixesToOverride;
this.configuration = configuration;
}
读取的preffixOverrides/suffixOverrides属性可以是符合以|为分隔符的字符串,比如"and | or"会被解析为List["AND","OR"]形式
笔者此处再简单看下apply()方法,代码如下
public boolean apply(DynamicContext context) {
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
// first,parse nested nodes
boolean result = contents.apply(filteredDynamicContext);
// aim to prefixOverrides and suffixOverrides,generate corrent sql
filteredDynamicContext.applyAll();
return result;
}
经过上述的代码执行后,生成的SQL语句均是转为大写形式的~~~
WhereHandler
用于解析where标签节点
private class WhereHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
// same as TrimSqlNode
WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
targetContents.add(where);
}
}
WhereSqlNode
经过查看,其是TrimSqlNode的子类,简单看下其源码
public class WhereSqlNode extends TrimSqlNode {
private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
public WhereSqlNode(Configuration configuration, SqlNode contents) {
super(configuration, contents, "WHERE", prefixList, null, null);
}
}
很简单,就是固定了参数prefix=WHERE和prefixOverrides=List["AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"],其余调用父类方法即可,详见上文
SetHandler
用于解析set标签节点
private class SetHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
// same as TrimSqlNode
SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
targetContents.add(set);
}
}
SetSqlNode
经过查看,其也是TrimSqlNode的子类,简单看下其源码
public class SetSqlNode extends TrimSqlNode {
private static List<String> suffixList = Arrays.asList(",");
public SetSqlNode(Configuration configuration,SqlNode contents) {
super(configuration, contents, "SET", null, null, suffixList);
}
}
很简单,就是固定了参数prefix=SET、suffix=null(会将suffixOverrides符合的条件置为空)、suffixOverrides=List[","],其余调用父类方法即可,详见上文
ForEachHandler
用于解析foreach标签节点
private class ForEachHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
// read collection/item/index/open/close/separator properties
String collection = nodeToHandle.getStringAttribute("collection");
String item = nodeToHandle.getStringAttribute("item");
String index = nodeToHandle.getStringAttribute("index");
String open = nodeToHandle.getStringAttribute("open");
String close = nodeToHandle.getStringAttribute("close");
String separator = nodeToHandle.getStringAttribute("separator");
// independent SqlNode
ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
targetContents.add(forEachSqlNode);
}
}
foreach节点的属性简析如下
- collection 代表的是集合的类型,例如list代表参数类型为List.class,array代表参数类型为数组类型
- item 代表集合的value值
- index 代表集合的key值,可为下标值也可为HashMap中的key值
- open 类似于prefix属性
- close 类似于suffix属性
- separator 拼装的分隔符号,多为","
ForEachSqlNode
独立的类,限于比较复杂,笔者此处只查看下其构造函数,有兴趣的读者可自行分析
public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
//解析帮助类
this.evaluator = new ExpressionEvaluator();
// 集合别名
this.collectionExpression = collectionExpression;
this.contents = contents;
this.open = open;
this.close = close;
this.separator = separator;
this.index = index;
this.item = item;
this.configuration = configuration;
}
IfHandler
用于解析if标签节点
private class IfHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
// read test properties
String test = nodeToHandle.getStringAttribute("test");
//
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
IfSqlNode
条件判断解析类,内部代码很简单,如下
public class IfSqlNode implements SqlNode {
private ExpressionEvaluator evaluator;
private String test;
private SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
public boolean apply(DynamicContext context) {
//主要作用即是用于条件的判断
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
}
org.apache.ibatis.scripting.xmltags.ExpressionEvaluator主要是应用OGNL语法进行解析类似name !=null
,其会读取上下文中是否有对应的属性值。具体的读者可自行分析
OtherwiseHandler/ChooseHandler
用于解析otherwise/choose/when节点,这三者一般搭配使用
1.OtherwiseHandler
private class OtherwiseHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
targetContents.add(mixedSqlNode);
}
}
2.ChooseHandler,内含choose/when的解析
private class ChooseHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> whenSqlNodes = new ArrayList<SqlNode>();
List<SqlNode> otherwiseSqlNodes = new ArrayList<SqlNode>();
//解析choose...when..otherwise结构
handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
//检查otherwise标签是否只有一个,大于一个则报错
SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
targetContents.add(chooseSqlNode);
}
// when标签使用IfHandler解析,otherwise标签使用OtherwiseHandler
private void handleWhenOtherwiseNodes(XNode chooseSqlNode, List<SqlNode> ifSqlNodes, List<SqlNode> defaultSqlNodes) {
List<XNode> children = chooseSqlNode.getChildren();
for (XNode child : children) {
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlers.get(nodeName);
if (handler instanceof IfHandler) {
handler.handleNode(child, ifSqlNodes);
} else if (handler instanceof OtherwiseHandler) {
handler.handleNode(child, defaultSqlNodes);
}
}
}
private SqlNode getDefaultSqlNode(List<SqlNode> defaultSqlNodes) {
SqlNode defaultSqlNode = null;
if (defaultSqlNodes.size() == 1) {
defaultSqlNode = defaultSqlNodes.get(0);
} else if (defaultSqlNodes.size() > 1) {
throw new BuilderException("Too many default (otherwise) elements in choose statement.");
}
return defaultSqlNode;
}
}
ChooseSqlNode
简单看下其构造函数
public class ChooseSqlNode implements SqlNode {
private SqlNode defaultSqlNode;
private List<SqlNode> ifSqlNodes;
public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
this.ifSqlNodes = ifSqlNodes;
this.defaultSqlNode = defaultSqlNode;
}
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : ifSqlNodes) {
if (sqlNode.apply(context)) {
return true;
}
}
if (defaultSqlNode != null) {
defaultSqlNode.apply(context);
return true;
}
return false;
}
}
ChooseSqlNode存放多个IfSqlNode和单个TextSqlNode/StaticTextSqlNode,
而choose..when..otherwise结构类似于java的switch..case结构
总结
对本文提及的内容作下简单的小结
where/set标签均可看做是trim标签的子类
choose与when/otherwise搭配使用,可以有多个when标签,只允许至多单个otherwise标签
除了bind标签,其余标签底下均可以有多个其他标签
TextSqlNode/StaticTextSqlNode可以说是CRUD解析sql的基础类
_parameter属性表示DAO接口方法对应的入参