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.Listcontents属性,主要作用是保存一个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=WHEREprefixOverrides=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=SETsuffix=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结构

总结

对本文提及的内容作下简单的小结

  1. where/set标签均可看做是trim标签的子类

  2. choosewhen/otherwise搭配使用,可以有多个when标签,只允许至多单个otherwise标签

  3. 除了bind标签,其余标签底下均可以有多个其他标签

  4. TextSqlNode/StaticTextSqlNode可以说是CRUD解析sql的基础类

  5. _parameter属性表示DAO接口方法对应的入参

上一篇:jsp获得文件的绝对路径


下一篇:iOS中获取各种文件的目录路径的方法