通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文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接口方法对应的入参

Spring mybatis源码篇章-动态SQL节点源码深入的更多相关文章

  1. Spring mybatis源码篇章-动态SQL基础语法以及原理

    通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-Mybatis的XML文件加载 前话 前文通过Spring中配置mapperLocations属性来进行对m ...

  2. Spring mybatis源码篇章-NodeHandler实现类具体解析保存Dynamic sql节点信息

    前言:通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-XMLLanguageDriver解析sql包装为SqlSource SqlNode接口类 publi ...

  3. Spring mybatis源码篇章-sql mapper配置文件绑定mapper class类

    前言:通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-MybatisDAO文件解析(二) 背景知识 MappedStatement是mybatis操作sql ...

  4. Spring mybatis源码篇章-XMLLanguageDriver解析sql包装为SqlSource

    前言:通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-MybatisDAO文件解析(二) 首先了解下sql mapper的动态sql语法 具体的动态sql的 ...

  5. Spring mybatis源码篇章-MapperScannerConfigurer关联dao接口

    前言:Spring针对Mybatis的XML方式的加载MappedStatement,通过引入MapperScannerConfigurer扫描类来关联相应的dao接口以供Service层调用.承接前 ...

  6. Spring mybatis源码篇章-MybatisDAO文件解析(二)

    前言:通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-MybatisDAO文件解析(一) 默认加载mybatis主文件方式 XMLConfigBuilder ...

  7. Spring mybatis源码篇章-MybatisDAO文件解析(一)

    前言:通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-SqlSessionFactory 加载指定的mybatis主文件 Mybatis模板文件,其中的属性 ...

  8. Spring mybatis源码篇章-Mybatis的XML文件加载

    通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-Mybatis主文件加载 前话 前文主要讲解了Mybatis的主文件加载方式,本文则分析不使用主文件加载方式 ...

  9. Spring mybatis源码篇章-Mybatis主文件加载

    通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-SqlSessionFactory 前话 本文承接前文的内容继续往下扩展,通过Spring与Mybatis的 ...

随机推荐

  1. PHP mysqli_error() 函数

    返回最近调用函数的最后一个错误描述: <?php  // 假定数据库用户名:root,密码:123456,数据库:RUNOOB  $con=mysqli_connect("localh ...

  2. 002_UCOSIII任务创建于删除

    (一)先创建一个启动任务来进行创建其它任务,创建任务的宏定义 #define START_TASK_PRIO 3 //任务优先级 #define START_STK_SIZE 128 //任务堆栈大小 ...

  3. 富文本编辑器粘贴word

    很多时候我们用一些管理系统的时候,发布新闻.公告等文字类信息时,希望能很快的将word里面的内容直接粘贴到富文本编辑器里面,然后发布出来.减少排版复杂的工作量. 下面是借用百度doc 来快速实现这个w ...

  4. 2018-2019 ACM-ICPC, Asia Jiaozuo Regional Contest

    目录 Contest Info Solutions A. Xu Xiake in Henan Province D. Keiichi Tsuchiya the Drift King E. Resist ...

  5. cdh版hbase构建Phoenix 遇到的坑

    Phoenix 构建cdh版hbase遇到的坑 1. 安装phoenix 下载:在github上下载对应版本https://github.com/apache/phoenix 解压:略 编译: 修改根 ...

  6. Android Jenkins 自动化打包构建

    前言 在测试app项目过程中,通常都是需要开发打测试包给到测试,但是无论是iOS还是Android的打包过程都是相当漫长的,频繁的回归测试需要频繁的打包,对于开发同学影响还是蛮大的.因此在这种情况下, ...

  7. gitlab配置邮箱postfix(新用户激活邮件)

    亲测可用 https://www.cnblogs.com/yoyoketang/p/10287345.html

  8. windows下使用curl命令&&常用curl命令

    什么是curl命令? curl是利用URL语法在命令行方式下工作的开源文件传输工具.它被广泛应用在Unix.多种Linux发行版中,并且有DOS和Win32.Win64下的移植版本. 如何在windo ...

  9. pwn学习日记Day12 《程序员的自我修养》读书笔记

    目标文件里有什么 ELF各段 代码段 text 数据段 data bss段 只读数据段 rodata 注释信息段 comment 堆栈提示段 .note.GNU-stack comment 存放编译器 ...

  10. 学号20175313 《实现Linux下Sort -t : -k 2功能》第十二周

    目录 一.题目要求 二.题目理解 三.设计思路 四.代码实现 五.代码链接 六.运行结果截图 七.参考资料 一.题目要求 实现Linux下Sort -t : -k 2的功能 二.题目理解 -t 分隔符 ...