介绍
SqlNode是mybatis动态SQL功能的重要组成部分,关于动态sql,官网对其的介绍如下:
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
例如下面这段sql中,<if></if>
标签所包含的内容则是mybatis动态sql功能最基本的体现,他能实现如果不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
mybatis提供的动态sql类型有:
- if
- choose
- when
- otherwise
- trim
- where
- set
- foreach
- script
- bind
本文章的目的在于从源码实现的角度分析mybatis框架
- 是如何解析用户关于动态sql的配置
- 动态sql在内存中的数据结构是什么
SqlSource和SqlNode的关系
文章开始之前需要搞明白代码中的SqlSource和SqlNode分别是什么,以及他们的关系是什么。这两个关系搞不清的话,看代码就会比较懵逼。
他们的关系如上图所示,SqlSource和SqlNode的关系是一对多的关系。
SqlNode
如上所述,SqlNode是动态sql配置在程序中的组织形式,每个 XML Node 会解析成对应的 SqlNode 对象。其类图如下:
SqlNode的各个实现类使用了组合设计模式,mybatis通过使用组合设计模式帮助上层调用者屏蔽了对象的复杂性,也使得如果后续需要添加新的SqlNode类型的话,会变得非常容易,符合开闭原则。
关于组合设计模式,可以参考这篇文章https://www.runoob.com/design-pattern/composite-pattern.html
SqlNode接口
public interface SqlNode {
/**
*
* @param context 上下文,在执行时该对象会持有用户传入的实际参数
* @return 当前 SQL Node 节点是否应用成功
*/
boolean apply(DynamicContext context);
}
IfSqlNode
对应于下面例子中的<if></if>
节点
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
IfSqlNode类的定义
public class IfSqlNode implements SqlNode {
//表达式解析器,用于判断 "if test='条件'"是否成立
private final ExpressionEvaluator evaluator;
//成立条件
private final String test;
//if节点的子节点
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
/**
* 如果当前if节点的条件成立,则判断子节点的条件是否成立
* @param context 上下文,在执行时该对象会持有用户传入的实际参数
* @return
*/
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
//子节点是否成立不能影响当前节点的判断结果,所以这里没有直接返回
contents.apply(context);
return true;
}
return false;
}
}
StaticTextSqlNode
StaticTextSqlNode表示的是文本类型,其中的文本不包含任何动态元素,即不包括${}这种占位符。对应于下图红框中的内容
StaticTextSqlNode类的定义:
public class StaticTextSqlNode implements SqlNode {
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
//因为StaticTextSqlNode表示不包含任何动态元素的节点,所以不需要对其做任何处理,
//直接将sql追加上下文对象中即可
@Override
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
}
TextSqlNode
TextSqlNode表示的是包含${}占位符的动态字符串节点,对应于下图中的红框部分:
TextSqlNode类的定义:
public class TextSqlNode implements SqlNode {
//包含占位符的原始文本字符串
private final String text;
//校验实参的正则对象
private final Pattern injectionFilter;
public TextSqlNode(String text) {
//调用重载的构造函数
this(text, null);
}
public TextSqlNode(String text, Pattern injectionFilter) {
this.text = text;
this.injectionFilter = injectionFilter;
}
/**
* 判断当前文本字符串是否包含 ${} 占位符,各种判断字符串中是否存在占位符 ${}
* @return
*/
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}
/**
* 从上下文中获取用户传入的实际参数,并使用实际参数替换对应的占位符
* 再通过 injectionFilter 字段所表示的正则对实参进行校验,最后将处理后的sql字符串添加到上下文中。
* @param context 上下文,在执行时该对象会持有用户传入的实际参数
* @return
*/
@Override
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);
}
private static class BindingTokenParser implements TokenHandler {
private DynamicContext context;
private Pattern injectionFilter;
public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
this.context = context;
this.injectionFilter = injectionFilter;
}
@Override
public String handleToken(String content) {
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {//是否为简单类型
context.getBindings().put("value", parameter);
}
//从上下文中获取实际参数
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
//通过正则匹配的方式检查实参是否合法
checkInjection(srtValue);
return srtValue;
}
private void checkInjection(String value) {
if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
}
}
}
private static class DynamicCheckerTokenParser implements TokenHandler {
private boolean isDynamic;
public DynamicCheckerTokenParser() {
// Prevent Synthetic Access
}
public boolean isDynamic() {
return isDynamic;
}
@Override
public String handleToken(String content) {
this.isDynamic = true;
return null;
}
}
}
ChooseSqlNode
choose元素和java中的switch语句相似,对应于下图红框中的内容:
ChooseSqlNode类的定义:
/**
* 由<choose></choose>标签的DTD定义可知,<when></when>标签可以有多个,而<otherwise></otherwise>只能有
* 一个。所以defaultSqlNode为一个对象,而ifSqlNodes是一个集合。
*/
public class ChooseSqlNode implements SqlNode {
//对应于<otherwise></otherwise>
private final SqlNode defaultSqlNode;
//对应于<when></when>
private final List<SqlNode> ifSqlNodes;
public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
this.ifSqlNodes = ifSqlNodes;
this.defaultSqlNode = defaultSqlNode;
}
@Override
public boolean apply(DynamicContext context) {
//遍历ifSqlNodes集合,查找条件成立的SqlNode
for (SqlNode sqlNode : ifSqlNodes) {
if (sqlNode.apply(context)) {
return true;
}
}
//如果ifSqlNodes集合中的所有节点的条件都不成立,则使用该兜底方案
if (defaultSqlNode != null) {
defaultSqlNode.apply(context);
return true;
}
return false;
}
}
VarDeclSqlNode
对应于<bind></bind>
标签,bind
元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。比如:
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
VarDeclSqlNode类的定义:
public class VarDeclSqlNode implements SqlNode {
//对应<bind/>的name属性
private final String name;
//对应<bind/>的value属性
private final String expression;
public VarDeclSqlNode(String var, String exp) {
name = var;
expression = exp;
}
@Override
public boolean apply(DynamicContext context) {
//从Ognl缓存中获取值
final Object value = OgnlCache.getValue(expression, context.getBindings());
context.bind(name, value);
return true;
}
}
MixedSqlNode
MixedSqlNode可以理解为组合模式中的树枝,其他类型的SqlNode都是树叶,从其类的定义中可以看到,他只是持有了很多个其他SqlNode对象而已。
MixedSqlNode类的定义:
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
}
ForEachSqlNode
<foreach/>
的介绍如下:
foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!
提示 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
对应于下图中的红框部分:
ForEachSqlNode类的定义:
public class ForEachSqlNode implements SqlNode {
public static final String ITEM_PREFIX = "__frch_";
//表达式解析器
private final ExpressionEvaluator evaluator;
//<foreach/>的collection属性
private final String collectionExpression;
//<foreach/>标签的内容所封装的SqlNode对象
private final SqlNode contents;
//<foreach/>的open属性
private final String open;
//<foreach/>的close属性
private final String close;
//<foreach/>的separator属性
private final String separator;
//<foreach/>的item属性
private final String item;
//<foreach/>的index属性
private final String index;
//全局配置对象
private final Configuration configuration;
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;
}
@Override
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
//获得表达式对应的集合,无论expression对应的实际类型是数组还是map,最后都会给处理成集合的形式
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
//如果为空集合,则直接返回true
if (!iterable.iterator().hasNext()) {
return true;
}
//是否为集合的第一个元素的标志位
boolean first = true;
//拼接"("
applyOpen(context);
//遍历的索引
int i = 0;
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first || separator == null) {
//如果是第一个元素,则不需要对其添加分隔符
context = new PrefixedContext(context, "");
} else {
//其余元素都需要添加分隔符
context = new PrefixedContext(context, separator);
}
//因为集合中的每一个元素都会有一个唯一对应的PrefixedContext对象,所以这个number可以认为在当前集合
//中是唯一的
int uniqueNumber = context.getUniqueNumber();
//转换在上下文中的占位符
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}
//处理子SqlNode节点
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
//追加")"
applyClose(context);
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}
//处理索引与实参之间的关系
private void applyIndex(DynamicContext context, Object o, int i) {
//如果遍历的是Map,则实参为集合的key
if (index != null) {
//绑定索引和实际对象的关系
context.bind(index, o);
//__frch_+index+i ---> 实际对象
context.bind(itemizeItem(index, i), o);
}
}
//处理item与实参的关系
private void applyItem(DynamicContext context, Object o, int i) {
//如果遍历的是Map,则实参为集合的value
if (item != null) {
// item ---> 实际对象
context.bind(item, o);
// __frch_+item+i ---> 实际对象
context.bind(itemizeItem(item, i), o);
}
}
//直接追加open,也就是 "("
private void applyOpen(DynamicContext context) {
if (open != null) {
context.appendSql(open);
}
}
//直接追加close,也就是 "("
private void applyClose(DynamicContext context) {
if (close != null) {
context.appendSql(close);
}
}
//拼接成 __frch_+index+i 的格式
private static String itemizeItem(String item, int i) {
return ITEM_PREFIX + item + "_" + i;
}
/**
* 继承自DynamicContext类,重写了父类的appendSql方法
* 是父类DynamicContext的静态代理类
*/
private static class FilteredDynamicContext extends DynamicContext {
private final DynamicContext delegate;
private final int index;
private final String itemIndex;
private final String item;
public FilteredDynamicContext(Configuration configuration,DynamicContext delegate, String itemIndex, String item, int i) {
super(configuration, null);
this.delegate = delegate;
this.index = i;
this.itemIndex = itemIndex;
this.item = item;
}
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}
@Override
public String getSql() {
return delegate.getSql();
}
/**
* 将#{item} 拼接成 #{__frch_+item+i} 的形式
* @param sql
*/
@Override
public void appendSql(String sql) {
GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
if (itemIndex != null && newContent.equals(content)) {
newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
}
return "#{" + newContent + "}";
});
delegate.appendSql(parser.parse(sql));
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
}
/**
* 继承自DynamicContext类,重写了父类的appendSql方法
* 是父类DynamicContext的静态代理类
*/
private class PrefixedContext extends DynamicContext {
private final DynamicContext delegate;
private final String prefix;
//表示前缀是否已经处理过
private boolean prefixApplied;
public PrefixedContext(DynamicContext delegate, String prefix) {
super(configuration, null);
this.delegate = delegate;
this.prefix = prefix;
this.prefixApplied = false;
}
public boolean isPrefixApplied() {
return prefixApplied;
}
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}
/**
* 在追加sql之前先拼接一个前缀
* @param sql 待处理的sql
*/
@Override
public void appendSql(String sql) {
if (!prefixApplied && sql != null && sql.trim().length() > 0) {
//先添加前缀
delegate.appendSql(prefix);
prefixApplied = true;
}
delegate.appendSql(sql);
}
@Override
public String getSql() {
return delegate.getSql();
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
}
}
TrimSqlNode
对于下面的例子:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样:
SELECT * FROM BLOG
WHERE
这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:
SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’
这个查询也会失败。这个问题不能简单地用条件元素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。
MyBatis 提供了一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。
用于动态更新语句的类似解决方案叫做 set。set 元素可以用于动态包含需要更新的列,忽略其它不更新的列。
TrimSqlNode类的定义:
public class TrimSqlNode implements SqlNode {
//<trim></trim>标签内部定义的SqlNode节点
private final SqlNode contents;
//前缀
private final String prefix;
//后缀
private final String suffix;
//需要被忽略的前缀值
private final List<String> prefixesToOverride;
//需要被忽略的后缀值
private final List<String> suffixesToOverride;
//全局配置对象
private final Configuration configuration;
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) {
this.contents = contents;
this.prefix = prefix;
this.prefixesToOverride = prefixesToOverride;
this.suffix = suffix;
this.suffixesToOverride = suffixesToOverride;
this.configuration = configuration;
}
@Override
public boolean apply(DynamicContext context) {
//对DynamicContext对象进行包装
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
//将包装过的上下文对象传递给子SqlNode节点
boolean result = contents.apply(filteredDynamicContext);
filteredDynamicContext.applyAll();
return result;
}
/**
* 将 a|b|c 类型的字符串转换成对应的字符串(大写)集合
* @param overrides
* @return
*/
private static List<String> parseOverrides(String overrides) {
if (overrides != null) {
final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
final List<String> list = new ArrayList<>(parser.countTokens());
while (parser.hasMoreTokens()) {
list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
}
return list;
}
return Collections.emptyList();
}
/**
* 继承自DynamicContext类,扩展了DynamicContext类的能力
*
*/
private class FilteredDynamicContext extends DynamicContext {
private DynamicContext delegate;
//前缀是否已被处理
private boolean prefixApplied;
//后缀是否已被处理
private boolean suffixApplied;
private StringBuilder sqlBuffer;
public FilteredDynamicContext(DynamicContext delegate) {
super(configuration, null);
this.delegate = delegate;
this.prefixApplied = false;
this.suffixApplied = false;
this.sqlBuffer = new StringBuilder();
}
//批量处理前缀和后缀,并将处理过的sql添加到上下文对象中
public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
//转为大写
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
applyPrefix(sqlBuffer, trimmedUppercaseSql);
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
delegate.appendSql(sqlBuffer.toString());
}
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
@Override
public void appendSql(String sql) {
sqlBuffer.append(sql);
}
@Override
public String getSql() {
return delegate.getSql();
}
//处理前缀
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
if (!prefixApplied) {
prefixApplied = true;
if (prefixesToOverride != null) {
for (String toRemove : prefixesToOverride) {
if (trimmedUppercaseSql.startsWith(toRemove)) {
sql.delete(0, toRemove.trim().length());//删除sql中对应的前缀
break;
}
}
}
//如果前缀不为空,则设置前缀,例如使用<where></where>标签时,
//mybatis会在所有的条件前面拼接where关键字,从而保证sql语法的正确性
if (prefix != null) {
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
}
//处理后缀
private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
if (!suffixApplied) {
suffixApplied = true;
if (suffixesToOverride != null) {
for (String toRemove : suffixesToOverride) {
if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
int start = sql.length() - toRemove.trim().length();
int end = sql.length();
sql.delete(start, end);
break;
}
}
}
//如果后缀不为空,则追加后缀
if (suffix != null) {
sql.append(" ");
sql.append(suffix);
}
}
}
}
}
WhereSqlNode
WhereSqlNode继承自TrimSqlNode类,其对需要忽略的前缀做了定义。WhereSqlNode类的定义如下:
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);
}
}
SetSqlNode
同样,SetSqlNode也是继承自TrimSqlNode类,它也对需要忽略的前缀做了定义。TrimSqlNode类的定义如下:
public class SetSqlNode extends TrimSqlNode {
private static final List<String> COMMA = Collections.singletonList(",");
public SetSqlNode(Configuration configuration,SqlNode contents) {
super(configuration, contents, "SET", COMMA, null, COMMA);
}
}
SqlNode配置的解析
通过前面的文章可以知道,xxxMapper.xml文件中sql语句的解析是在 XMLStatementBuilder类中完成的,当XMLStatementBuilder类的parseStatementNode方法在解析insert、update、delete和select语句的时候,会触发对sqlNode和sqlSource的解析,入口代码如下:
XMLStatementBuilder#parseStatementNode
在XMLLanguageDriver#createSqlSource代码如下:
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
这里通过创建一个XMLScriptBuilder类,并调用其parseScriptNode方法完成对SqlSource的解析和封装
SqlNode的数据结构
SqlNode的数据结构很像一个树结构,MixedSqlNode节点为树枝,其余类型为树叶。其结构大致如下:
XMLScriptBuilder
查看XMLScriptBuilder类的定义可以发现,XMLScriptBuilde的父类是BaseBuilder类,这说明XMLScriptBuilde和前面介绍的XMLMapperBuilder、XMLStatementBuilder类似,都是通过建造者设计模式来完成对某些配置的解析。
字段
//解析的Xpath节点
private final XNode context;
//当前Xpath节点是否是动态节点
private boolean isDynamic;
private final Class<?> parameterType;
//节点处理器缓存
private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();
节点处理器马上会讲
构造函数
public XMLScriptBuilder(Configuration configuration, XNode context) {
//调用重载的构造函数
this(configuration, context, null);
}
public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
super(configuration);
//解析的Xpath节点
this.context = context;
//参数类型
this.parameterType = parameterType;
//初始化节点处理器缓存
initNodeHandlerMap();
}
初始化节点处理器缓存
初始化节点缓存的方法很简单,就是对每一种sqlNode都创建一个对应的处理器类,并将其保存在HashMap中。
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
parseScriptNode
/**
* 解析动态标签,并将解析后的结果封装成SqlSource对象返回
*/
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {//如过是动态节点,则返回DynamicSqlSource的实例
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {//否则返回RawSqlSource的实例
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
parseDynamicTags
用于解析<insert/>
、<update/>
、<select>
和<delete/>
内的动态节点
/**
* 解析动态节点
* @param node 待解析的xpath节点
* @return
*/
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
//获取当前xpath节点的所有子节点
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
//如果当前节点是文本节点或CDATA
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
//封装成一个TextSqlNode实例
TextSqlNode textSqlNode = new TextSqlNode(data);
//如果是动态节点,则将isDynamic标志位置为true
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {//不是动态节点
contents.add(new StaticTextSqlNode(data));
}
// 如果当前xpath节点还是一个节点
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
//获取当前节点的标签名称
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);
}
节点处理器
XMLScriptBuilder类中定义了一个接口(NodeHandler)和8个实现类,分别用于处理不同类型的SqlNode。他们都是XMLScriptBuilder类的内部类,其类图如下所示:
NodeHandler
该接口只定义了一个方法,该方法用于解析对应的SqlNode
/**
* 节点处理器接口
*/
private interface NodeHandler {
/**
* 处理节点
* @param nodeToHandle 待处理的Xpath节点
* @param targetContents 已解析的SqlNode集合
*/
void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}
IfHandler
用于解析<if></if>
节点的配置
private class IfHandler implements NodeHandler {
public IfHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//解析动态标签,获取树枝节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
//获取配置的test属性
String test = nodeToHandle.getStringAttribute("test");
//构建IfSqlNode实例
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
BindHandler
用于解析<bing></bing>
节点的配置,直接读取配置信息创建VarDeclSqlNode对象即可
private class BindHandler implements NodeHandler {
public BindHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
final String name = nodeToHandle.getStringAttribute("name");
final String expression = nodeToHandle.getStringAttribute("value");
final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
targetContents.add(node);
}
}
TrimHandler
用于解析<trim></trim>
节点的配置,直接读取配置信息创建TrimSqlNode对象
private class TrimHandler implements NodeHandler {
public TrimHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//解析动态标签,获取树枝节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String prefix = nodeToHandle.getStringAttribute("prefix");
String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
String suffix = nodeToHandle.getStringAttribute("suffix");
String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
}
WhereHandler
用于解析<where></where>
节点的配置,直接读取配置信息创建WhereSqlNode对象
private class WhereHandler implements NodeHandler {
public WhereHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//解析动态标签,获取树枝节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
targetContents.add(where);
}
}
SetHandler
用于解析<set></set>
节点的配置
private class SetHandler implements NodeHandler {
public SetHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
targetContents.add(set);
}
}
ForEachHandler
用于解析<foreach></foreach>
节点的配置
private class ForEachHandler implements NodeHandler {
public ForEachHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
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");
ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
targetContents.add(forEachSqlNode);
}
}
ChooseHandler
private class ChooseHandler implements NodeHandler {
public ChooseHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//when标签对应的SqlNode的集合
List<SqlNode> whenSqlNodes = new ArrayList<>();
//otherwise标签对应的SqlNode的集合
List<SqlNode> otherwiseSqlNodes = new ArrayList<>();
//处理 when 和 otherwise 节点
handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
//获取otherwise节点
SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
//创建ChooseSqlNode对象
ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
targetContents.add(chooseSqlNode);
}
//迭代集合,对集合中的每一个XNode进行处理
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 = nodeHandlerMap.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;
}
}