该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址Mybatis-Spring 源码分析 GitHub 地址Spring-Boot-Starter 源码分析 GitHub 地址)进行阅读

MyBatis 版本:3.5.2

MyBatis-Spring 版本:2.0.3

MyBatis-Spring-Boot-Starter 版本:2.1.4

MyBatis的初始化

在MyBatis初始化过程中,大致会有以下几个步骤:

  1. 创建Configuration全局配置对象,会往TypeAliasRegistry别名注册中心添加Mybatis需要用到的相关类,并设置默认的语言驱动类为XMLLanguageDriver

  2. 加载mybatis-config.xml配置文件、Mapper接口中的注解信息和XML映射文件,解析后的配置信息会形成相应的对象并保存到Configuration全局配置对象中

  3. 构建DefaultSqlSessionFactory对象,通过它可以创建DefaultSqlSession对象,MyBatis中SqlSession的默认实现类

因为整个初始化过程涉及到的代码比较多,所以拆分成了四个模块依次对MyBatis的初始化进行分析:

  • 《MyBatis初始化(一)之加载mybatis-config.xml》
  • 《MyBatis初始化(二)之加载Mapper接口与XML映射文件》
  • 《MyBatis初始化(三)之SQL初始化(上)》
  • 《MyBatis初始化(四)之SQL初始化(下)》

由于在MyBatis的初始化过程中去解析Mapper接口与XML映射文件涉及到的篇幅比较多,XML映射文件的解析过程也比较复杂,所以才分成了后面三个模块,逐步分析,这样便于理解

初始化(三)之SQL初始化(上)

在前面的MyBatis初始化相关文档中已经大致讲完了MyBatis初始化的整个流程,其中遗漏了一部分,就是在解析<select /> <insert /> <update /> <delete />节点的过程中,是如何解析SQL语句,如何实现动态SQL语句,最终会生成一个org.apache.ibatis.mapping.SqlSource对象的,对于这烦琐且易出错的过程,我们来看看MyBatis如何实现的?

我们回顾org.apache.ibatis.builder.xml.XMLStatementBuilderparseStatementNode()解析 Statement 节点时,通过下面的方法创建对应的SqlSource对象

// 创建对应的 SqlSource 对象,保存了该节点下 SQL 相关信息
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

langDriver是从Configuration全局配置对象中获取的默认实现类,对应的也就是XMLLanguageDriver,在Configuration初始化的时候设置的

public Configuration() {
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
languageRegistry.register(RawLanguageDriver.class);
}

主要包路径:org.apache.ibatis.scripting、org.apache.ibatis.builder、org.apache.ibatis.mapping

主要涉及到的类:

  • org.apache.ibatis.scripting.xmltags.XMLLanguageDriver:语言驱动接口的默认实现,创建ParameterHandler参数处理器对象和SqlSource资源对象

  • org.apache.ibatis.scripting.xmltags.XMLScriptBuilder:继承 BaseBuilder 抽象类,负责将SQL脚本(XML或者注解中定义的SQL语句)解析成SqlSource(DynamicSqlSource或者RawSqlSource)资源对象

  • org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.NodeHandler:定义在XMLScriptBuilder内部的一个接口,用于处理MyBatis自定义标签(<if /> <foreach />等),生成对应的SqlNode对象,不同的实现类处理不同的标签

  • org.apache.ibatis.scripting.xmltags.DynamicContext:解析动态SQL语句时的上下文,用于解析SQL时,记录动态SQL处理后的SQL语句,内部提供ContextMap对象保存上下文的参数

  • org.apache.ibatis.scripting.xmltags.SqlNode:SQL Node接口,每个XML Node会解析成对应的SQL Node对象,通过上下文可以对动态SQL进行逻辑处理,生成需要的结果

  • org.apache.ibatis.scripting.xmltags.OgnlCache:用于处理Ognl表达式

语言驱动接口的实现类如下图所示:

LanguageDriver

org.apache.ibatis.scripting.LanguageDriver:语言驱动接口,代码如下:

public interface LanguageDriver {

	/**
* Creates a {@link ParameterHandler} that passes the actual parameters to the the JDBC statement.
* 创建 ParameterHandler 对象
*
* @param mappedStatement The mapped statement that is being executed
* @param parameterObject The input parameter object (can be null)
* @param boundSql The resulting SQL once the dynamic language has been executed.
* @return 参数处理器
* @author Frank D. Martinez [mnesarco]
* @see DefaultParameterHandler
*/
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql); /**
* Creates an {@link SqlSource} that will hold the statement read from a mapper xml file.
* It is called during startup, when the mapped statement is read from a class or an xml file.
* 创建 SqlSource 对象,从 Mapper XML 配置的 Statement 标签中,即 <select /> 等。
*
* @param configuration The MyBatis configuration
* @param script XNode parsed from a XML file
* @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
* @return SQL 资源
*/
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType); /**
* Creates an {@link SqlSource} that will hold the statement read from an annotation.
* It is called during startup, when the mapped statement is read from a class or an xml file.
* 创建 SqlSource 对象,从方法注解配置,即 @Select 等。
*
* @param configuration The MyBatis configuration
* @param script The content of the annotation
* @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
* @return SQL 资源
*/
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}

定义了三个方法:

  1. createParameterHandler:获取 ParameterHandler 参数处理器对象

  2. createSqlSource:创建 SqlSource 对象,解析 Mapper XML 配置的 Statement 标签中,即 <select /> <update /> <delete /> <insert />

  3. createSqlSource:创建 SqlSource 对象,从方法注解配置,即 @Select 等

XMLLanguageDriver

org.apache.ibatis.scripting.xmltags.XMLLanguageDriver:语言驱动接口的默认实现,代码如下:

public class XMLLanguageDriver implements LanguageDriver {

  @Override
public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
// 创建 DefaultParameterHandler 对象
return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
} /**
* 用于解析 XML 映射文件中的 SQL
*
* @param configuration The MyBatis configuration
* @param script XNode parsed from a XML file
* @param parameterType input parameter type got from a mapper method or
* specified in the parameterType xml attribute. Can be
* null.
* @return SQL 资源
*/
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
// 创建 XMLScriptBuilder 对象,执行解析
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
} /**
* 用于解析注解中的 SQL
*
* @param configuration The MyBatis configuration
* @param script The content of the annotation
* @param parameterType input parameter type got from a mapper method or
* specified in the parameterType xml attribute. Can be
* null.
* @return SQL 资源
*/
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
// <1> 如果是 <script> 开头,表示是在注解中使用的动态 SQL
if (script.startsWith("<script>")) {
// <1.1> 创建 XPathParser 对象,解析出 <script /> 节点
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// issue #127
// <2.1> 变量替换
script = PropertyParser.parse(script, configuration.getVariables());
// <2.2> 创建 TextSqlNode 对象
TextSqlNode textSqlNode = new TextSqlNode(script);
if (textSqlNode.isDynamic()) { // <2.3.1> 如果是动态 SQL ,则创建 DynamicSqlSource 对象
return new DynamicSqlSource(configuration, textSqlNode);
} else { // <2.3.2> 如果非动态 SQL ,则创建 RawSqlSource 对象
return new RawSqlSource(configuration, script, parameterType);
}
}
}
}

实现了LanguageDriver接口:

  1. 创建 DefaultParameterHandler 默认参数处理器并返回

  2. 解析 XML 映射文件中的 SQL,通过创建 XMLScriptBuilder 对象,调用其 parseScriptNode() 方法解析

  3. 解析注解定义的 SQL

    1. 如果是 <script> 开头,表示是在注解中使用的动态 SQL,将其转换成 XNode 然后调用上述方法,不了解的可以看看MyBatis三种动态SQL配置方式
    2. 先将注解中定义的 SQL 中包含的变量进行转换,然后创建对应的 SqlSource 对象

RawLanguageDriver

org.apache.ibatis.scripting.defaults.RawLanguageDriver:继承了XMLLanguageDriver,在的基础上增加了是否为静态SQL语句的校验,也就是判断创建的 SqlSource 是否为 RawSqlSource 静态 SQL 资源

XMLScriptBuilder

org.apache.ibatis.scripting.xmltags.XMLScriptBuilder:继承 BaseBuilder 抽象类,负责将 SQL 脚本(XML或者注解中定义的 SQL )解析成 SqlSource 对象

构造方法

public class XMLScriptBuilder extends BaseBuilder {
/**
* 当前 SQL 的 XNode 对象
*/
private final XNode context;
/**
* 是否为动态 SQL
*/
private boolean isDynamic;
/**
* SQL 的 Java 入参类型
*/
private final Class<?> parameterType;
/**
* NodeNodeHandler 的映射
*/
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);
this.context = context;
this.parameterType = parameterType;
initNodeHandlerMap();
} 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());
}
}

在构造函数中会初始化 NodeHandler 处理器,分别用于处理不同的MyBatis自定义的XML标签,例如<if /> <where /> <foreach />等标签

parseScriptNode方法

parseScriptNode()方法将 SQL 脚本(XML或者注解中定义的 SQL )解析成 SqlSource 对象,代码如下:

public SqlSource parseScriptNode() {
// 解析 XML 或者注解中定义的 SQL
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
// 动态语句,使用了 ${} 也算
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
  1. 通过调用parseDynamicTags(XNode node)方法,将解析 SQL 成 MixedSqlNode 对象,主要是将一整个 SQL 解析成一系列的 SqlNode 对象
  2. 如果是动态SQL语句,使用了MyBatis自定义的XML标签(<if />等)或者使用了${},则封装成DynamicSqlSource对象
  3. 否则就是静态SQL语句,封装成RawSqlSource对象

parseDynamicTags方法

parseDynamicTags()将 SQL 脚本(XML或者注解中定义的 SQL )解析成MixedSqlNode对象,代码如下:

protected MixedSqlNode parseDynamicTags(XNode node) {
// <1> 创建 SqlNode 数组
List<SqlNode> contents = new ArrayList<>();
/*
* <2> 遍历 SQL 节点中所有子节点
* 这里会对该节点内的所有内容进行处理然后返回 NodeList 对象
* 1. 文本内容会被解析成 '<#text></#text>' 节点,就算一个换行符也会解析成这个
* 2. <![CDATA[ content ]]> 会被解析成 '<#cdata-section>content</#cdata-section>' 节点
* 3. 其他动态<if /> <where />
*/
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
// 当前子节点
XNode child = node.newXNode(children.item(i));
// <2.1> 如果类型是 Node.CDATA_SECTION_NODE 或者 Node.TEXT_NODE 时
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE // <![CDATA[ ]]>节点
|| child.getNode().getNodeType() == Node.TEXT_NODE) { // 纯文本
// <2.1.1> 获得内容
String data = child.getStringBody("");
// <2.1.2> 创建 TextSqlNode 对象
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) { // <2.1.2.1> 如果是动态的 TextSqlNode 对象,也就是使用了 '${}'
// 添加到 contents 中
contents.add(textSqlNode);
// 标记为动态 SQL
isDynamic = true;
} else { // <2.1.2.2> 如果是非动态的 TextSqlNode 对象,没有使用 '${}'
// <2.1.2> 创建 StaticTextSqlNode 添加到 contents 中
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628 <2.2> 如果类型是 Node.ELEMENT_NODE
// <2.2.1> 根据子节点的标签,获得对应的 NodeHandler 对象
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) { // 获得不到,说明是未知的标签,抛出 BuilderException 异常
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// <2.2.2> 执行 NodeHandler 处理
handler.handleNode(child, contents);
// <2.2.3> 标记为动态 SQL
isDynamic = true;
}
}
// <3> 创建 MixedSqlNode 对象
return new MixedSqlNode(contents);
}

<1> 创建 SqlNode 数组 contents,用于保存解析 SQL 后的一些列 SqlNode 对象

<2> 获取定义的 SQL 节点中所有子节点,返回一个 NodeList 对象,这个对象中包含了该 SQL 节点内的所有信息,然后逐个遍历子节点

1. 其中文本内容会被解析成`<#text></#text>`节点,就算一个换行符也会解析成这个
2. `<![CDATA[ ]]>` 会被解析成 `<#cdata-section></#cdata-section>` 节点
3. 还有其他MyBatis自定义的标签`<if /> <where />`等等

<2.1> 如果子节点是<#text />或者<#cdata-section />类型

<2.1.1> 获取子节点的文本内容

<2.1.2> 创建 TextSqlNode 对象

<2.1.2.1> 调用 TextSqlNode 的 isDynamic() 方法,点击去该进去看看就知道了,如果文本中使用了${},则标记为动态 SQL 语句,将其添加至 contents 数组中

<2.1.2.2> 否则就是静态文本内容,创建对应的 StaticTextSqlNode 对象,将其添加至 contents 数组中

<2.2> 如果类型是 Node.ELEMENT_NODE 时,也就是 MyBatis 的自定义标签

<2.2.1> 根据子节点的标签名称,获得对应的 NodeHandler 对象

<2.2.2> 执行NodeHandlerhandleNode方法处理该节点,创建不通类型的 SqlNode 并添加到 contents 数组中,如何处理的在下面讲述

<2.2.3> 标记为动态 SQL 语句

<3> 最后将创建 contents 封装成 MixedSqlNode 对象

NodeHandler

XMLScriptBuilder的内部接口,用于处理MyBatis自定义标签,接口实现类如下图所示:

代码如下:

private interface NodeHandler {
/**
* 处理 Node
*
* @param nodeToHandle 要处理的 XNode 节点
* @param targetContents 目标的 SqlNode 数组。实际上,被处理的 XNode 节点会创建成对应的 SqlNode 对象,添加到 targetContents 中
*/
void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}

这些 NodeHandler 实现类都定义在 XMLScriptBuilder 内部,用于处理不同标签,我们逐个来看

BindHandler

实现了NodeHandler接口,<bind />标签的处理器,代码如下:

/**
* <bind />元素允许你在 OGNL 表达式(SQL语句)以外创建一个变量,并将其绑定到当前的上下文
*/
private class BindHandler implements NodeHandler {
public BindHandler() {
// Prevent Synthetic Access
}
@Override
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);
}
}
  1. 获取<bind />标签的name和value属性

  2. 根据这些属性创建一个 VarDeclSqlNode 对象

  3. 添加到targetContents集合中

例如这样配置:

<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>

TrimHandler

实现了NodeHandler接口,<trim />标签的处理器,代码如下:

private class TrimHandler implements NodeHandler {
public TrimHandler() {
// Prevent Synthetic Access
} @Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// <1> 解析内部的 SQL 节点,成 MixedSqlNode 对象
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// <2> 获得 prefix、prefixOverrides、"suffix"、suffixOverrides 属性
String prefix = nodeToHandle.getStringAttribute("prefix");
String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
String suffix = nodeToHandle.getStringAttribute("suffix");
String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
// <3> 创建 TrimSqlNode 对象
TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
}
  1. 继续调用parseDynamicTags方法解析<if />标签内部的子标签节点,嵌套解析,生成MixedSqlNode对象

  2. 获得 prefixprefixOverridessuffixsuffixOverrides 属性

  3. 根据上面获取到的属性创建TrimSqlNode对象

  4. 添加到targetContents集合中

WhereHandler

实现了NodeHandler接口,<where />标签的处理器,代码如下:

private class WhereHandler implements NodeHandler {
public WhereHandler() {
// Prevent Synthetic Access
} @Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析内部的 SQL 节点,成 MixedSqlNode 对象
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 创建 WhereSqlNode 对象
WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
targetContents.add(where);
}
}
  1. 继续调用parseDynamicTags方法解析<where />标签内部的子标签节点,嵌套解析,生成MixedSqlNode对象
  2. 创建WhereSqlNode对象,该对象继承了TrimSqlNode,自定义前缀(WHERE)和需要删除的前缀(AND、OR等)
  3. 添加到targetContents集合中

SetHandler

实现了NodeHandler接口,<set />标签的处理器,代码如下:

private class SetHandler implements NodeHandler {
public SetHandler() {
// Prevent Synthetic Access
} @Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析内部的 SQL 节点,成 MixedSqlNode 对象
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
targetContents.add(set);
}
}
  1. 继续调用parseDynamicTags方法解析<set />标签内部的子标签节点,嵌套解析,生成MixedSqlNode对象
  2. 创建SetSqlNode对象,该对象继承了TrimSqlNode,自定义前缀(SET)和需要删除的前缀和后缀(,)
  3. 添加到targetContents集合中

ForEachHandler

实现了NodeHandler接口,<foreach />标签的处理器,代码如下:

private class ForEachHandler implements NodeHandler {
public ForEachHandler() {
// Prevent Synthetic Access
} @Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析内部的 SQL 节点,成 MixedSqlNode 对象
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 获得 collection、item、index、open、close、separator 属性
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 forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
targetContents.add(forEachSqlNode);
}
}
  1. 继续调用parseDynamicTags方法解析<foreach />标签内部的子标签节点,嵌套解析,生成MixedSqlNode对象
  2. 获得 collection、item、index、open、close、separator 属性
  3. 根据这些属性创建ForEachSqlNode对象
  4. 添加到targetContents集合中

IfHandler

实现了NodeHandler接口,<if />标签的处理器,代码如下:

private class IfHandler implements NodeHandler {
public IfHandler() {
// Prevent Synthetic Access
} @Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析内部的 SQL 节点,成 MixedSqlNode 对象
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 获得 test 属性
String test = nodeToHandle.getStringAttribute("test");
// 创建 IfSqlNode 对象
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
  1. 继续调用parseDynamicTags方法解析<if />标签内部的子标签节点,嵌套解析,生成MixedSqlNode对象
  2. 获得 test 属性
  3. 根据这个属性创建IfSqlNode对象
  4. 添加到targetContents集合中

OtherwiseHandler

实现了NodeHandler接口,<otherwise />标签的处理器,代码如下:

private class OtherwiseHandler implements NodeHandler {
public OtherwiseHandler() {
// Prevent Synthetic Access
} @Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析内部的 SQL 节点,成 MixedSqlNode 对象
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
targetContents.add(mixedSqlNode);
}
}
  1. 继续调用parseDynamicTags方法解析<otherwise />标签内部的子标签节点,嵌套解析,生成MixedSqlNode对象
  2. 添加到targetContents集合中,需要结合ChooseHandler使用

ChooseHandler

实现了NodeHandler接口,<choose />标签的处理器,代码如下:

private class ChooseHandler implements NodeHandler {
public ChooseHandler() {
// Prevent Synthetic Access
} @Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> whenSqlNodes = new ArrayList<>();
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);
} 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) { // 处理 `<when />` 标签的情况
handler.handleNode(child, ifSqlNodes);
} else if (handler instanceof OtherwiseHandler) { // 处理 `<otherwise />` 标签的情况
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;
}
}
  1. 先逐步处理<choose />标签的<when /><otherwise />子标签们,通过组合 IfHandler 和 OtherwiseHandler 两个处理器,实现对子节点们的解析

  2. 如果存在<otherwise />子标签,则抛出异常

  3. 根据这些属性创建ChooseSqlNode对象

  4. 添加到targetContents集合中

DynamicContext

org.apache.ibatis.scripting.xmltags.DynamicContext:解析动态SQL语句时的上下文,用于解析SQL时,记录动态SQL处理后的SQL语句,内部提供ContextMap对象保存上下文的参数

构造方法

public class DynamicContext {

  /**
* 入参保存在 ContextMap 中的 Key
*
* {@link #bindings}
*/
public static final String PARAMETER_OBJECT_KEY = "_parameter";
/**
* 数据库编号保存在 ContextMap 中的 Key
*
* {@link #bindings}
*/
public static final String DATABASE_ID_KEY = "_databaseId"; static {
// <1.2> 设置 OGNL 的属性访问器
OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
} /**
* 上下文的参数集合,包含附加参数(通过`<bind />`标签生成的,或者`<foreach />`标签中的集合的元素等等)
*/
private final ContextMap bindings;
/**
* 生成后的 SQL
*/
private final StringJoiner sqlBuilder = new StringJoiner(" ");
/**
* 唯一编号。在 {@link org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.ForEachHandler} 使用
*/
private int uniqueNumber = 0; public DynamicContext(Configuration configuration, Object parameterObject) {
// <1> 初始化 bindings 参数
if (parameterObject != null && !(parameterObject instanceof Map)) {
// 构建入参的 MetaObject 对象
MetaObject metaObject = configuration.newMetaObject(parameterObject);
// 入参类型是否有对应的类型处理器
boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
bindings = new ContextMap(metaObject, existsTypeHandler);
} else {
bindings = new ContextMap(null, false);
}
// <2> 添加 bindings 的默认值
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
}
类型 属性 说明
ContextMap bindings 上下文的参数集合,包含附加参数(通过<bind />标签生成的,或者<foreach />标签解析参数保存的),以及几个默认值
StringJoiner sqlBuilder 保存本次解析后的SQL,每次添加字符串以空格作为分隔符
int uniqueNumber 唯一编号,在ForEachHandler处理节点时需要用到,生成唯一数组作为集合中每个元素的索引(作为后缀)
  1. 初始化bindings参数,创建 ContextMap 对象

    1. 根据入参转换成MetaObject对象
    2. 在静态代码块中,设置OGNL的属性访问器,OgnlRuntime 是 OGNL 库中的类,设置ContextMap对应的访问器是ContextAccessor
  2. bindings中添加几个默认值:_parameter > 入参对象,_databaseId -> 数据库标识符

ContextMap

DynamicContext的内部静态类,继承HashMap,用于保存解析动态SQL语句时的上下文的参数集合,代码如下:

static class ContextMap extends HashMap<String, Object> {
private static final long serialVersionUID = 2977601501966151582L;
/**
* parameter 对应的 MetaObject 对象
*/
private final MetaObject parameterMetaObject;
/**
* 是否有对应的类型处理器
*/
private final boolean fallbackParameterObject; public ContextMap(MetaObject parameterMetaObject, boolean fallbackParameterObject) {
this.parameterMetaObject = parameterMetaObject;
this.fallbackParameterObject = fallbackParameterObject;
} @Override
public Object get(Object key) {
String strKey = (String) key;
if (super.containsKey(strKey)) {
return super.get(strKey);
} if (parameterMetaObject == null) {
return null;
} if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) {
return parameterMetaObject.getOriginalObject();
} else {
// issue #61 do not modify the context when reading
return parameterMetaObject.getValue(strKey);
}
}
}

重写了 HashMap 的 get(Object key) 方法,增加支持对 parameterMetaObject 属性的访问

ContextAccessor

DynamicContext的内部静态类,实现 ognl.PropertyAccessor 接口,上下文访问器,代码如下:

static class ContextAccessor implements PropertyAccessor {
@Override
public Object getProperty(Map context, Object target, Object name) {
Map map = (Map) target; // 优先从 ContextMap 中,获得属性
Object result = map.get(name);
if (map.containsKey(name) || result != null) {
return result;
} // <x> 如果没有,则从 PARAMETER_OBJECT_KEY 对应的 Map 中,获得属性
Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
if (parameterObject instanceof Map) {
return ((Map) parameterObject).get(name);
} return null;
} @Override
public void setProperty(Map context, Object target, Object name, Object value) {
Map<Object, Object> map = (Map<Object, Object>) target;
map.put(name, value);
} @Override
public String getSourceAccessor(OgnlContext arg0, Object arg1, Object arg2) {
return null;
} @Override
public String getSourceSetter(OgnlContext arg0, Object arg1, Object arg2) {
return null;
}
}

在DynamicContext的静态代码块中,设置OGNL的属性访问器,设置了ContextMap.class的属性访问器为ContextAccessor

这里方法的入参中的target,就是 ContextMap 对象

  1. 在重写的getProperty方法中,先从 ContextMap 里面获取属性值(可以回过去看下ContextMap的get方法)

  2. 没有获取到则获取 PARAMETER_OBJECT_KEY 属性的值,如果是 Map 类型,则从这里面获取属性值

回看 DynamicContext 的构造方法,细品一下,先从Map中获取属性值,没有获取到则从parameterObject入参对象中获取属性值

SqlNode

org.apache.ibatis.scripting.xmltags.SqlNode:SQL Node接口,每个XML Node会解析成对应的SQL Node对象,通过上下文可以对动态SQL进行逻辑处理,生成需要的结果

实现类如下图所示:

代码如下:

public interface SqlNode {
/**
* 应用当前 SQLNode 节点
*
* @param context 正在解析 SQL 语句的上下文
* @return 是否应用成功
*/
boolean apply(DynamicContext context);
}

因为在解析SQL语句的时候我们需要根据入参来处理不同的SqlNode,通过其apply(DynamicContext context)方法应用SqlNode节点,将节点转换成相应的SQL

我们来看看它的实现类是如何处理相应的SQL Node的

VarDeclSqlNode

org.apache.ibatis.scripting.xmltags.VarDeclSqlNode:实现 SqlNode 接口,<bind /> 标签对应的 SqlNode 实现类,代码如下:

public class VarDeclSqlNode implements SqlNode {
/**
* 变量名称
*/
private final String name;
/**
* 表达式
*/
private final String expression; public VarDeclSqlNode(String var, String exp) {
name = var;
expression = exp;
} @Override
public boolean apply(DynamicContext context) {
// 获取该表达式转换后结果
final Object value = OgnlCache.getValue(expression, context.getBindings());
// 将该结果与变量名称设置到解析 SQL 语句的上下文中,这样接下来的解析过程中可以获取到 name 的值
context.bind(name, value);
return true;
}
}
  1. 通过OGNL表达式expression从DynamicContext上下文的ContextMap中获取转换后的结果,OgnlCache在后面讲到

  2. name与转换后的结果绑定到DynamicContext上下文中,后续处理其他节点可以获取到

TrimSqlNode

org.apache.ibatis.scripting.xmltags.TrimSqlNode:实现 SqlNode 接口,<trim/> 标签对应的 SqlNode 实现类

构造方法
public class TrimSqlNode implements SqlNode {
/**
* MixedSqlNode,包含该<if />节点内所有信息
*/
private final SqlNode contents;
/**
* 前缀,行首添加
*/
private final String prefix;
/**
* 后缀,行尾添加
*/
private final String suffix;
/**
* 需要删除的前缀,例如这样定义:'AND|OR'
* 注意空格,这里是不会去除的
*/
private final List<String> prefixesToOverride;
/**
* 需要删除的后缀,例如我们这样定义:',|AND'
* 注意空格,这里是不会去除的
*/
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;
}
}

在构造方法中解析<trim />标签的属性,其中调用了parseOverrides方法将|作为分隔符分隔该字符串并全部大写,生成一个数组,相关属性可查看上面的注释

apply方法
@Override
public boolean apply(DynamicContext context) {
// <1> 创建 FilteredDynamicContext 对象
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
// <2> 先解析 <trim /> 节点中的内容,将生成的 SQL 先存放在 FilteredDynamicContext 中
boolean result = contents.apply(filteredDynamicContext);
/*
* <3> 执行 FilteredDynamicContext 的应用
* 对上一步解析到的内容进行处理
* 处理完成后再将处理后的 SQL 拼接到 DynamicContext 中
*/
filteredDynamicContext.applyAll();
return result;
}
  1. 通过装饰器模式将context装饰成FilteredDynamicContext对象

  2. 因为<trim />标签中定义了内容或者其他标签,都会解析成相应的SqlNode,保存在contents中(MixedSqlNode)

    所以这里需要先应用内部的SqlNode,转换后的SQL会先保存在FilteredDynamicContext中

  3. FilteredDynamicContext中的SQL进行处理,也就是添加前后缀,去除前后缀的处理逻辑,然后将处理后的SQL拼接到context

FilteredDynamicContext

TrimSqlNode的私有内部类,继承了DynamicContext类,对<trim />标签逻辑的实现,代码如下:

private class FilteredDynamicContext extends DynamicContext {
/**
* 装饰的 DynamicContext 对象
*/
private DynamicContext delegate;
/**
* 是否 prefix 已经被应用
*/
private boolean prefixApplied;
/**
* 是否 suffix 已经被应用
*/
private boolean suffixApplied;
/**
* StringBuilder 对象
*
* @see #appendSql(String)
*/
private StringBuilder sqlBuffer; public FilteredDynamicContext(DynamicContext delegate) {
super(configuration, null);
this.delegate = delegate;
this.prefixApplied = false;
this.suffixApplied = false;
this.sqlBuffer = new StringBuilder();
} public void applyAll() {
// <1> 去除前后多余的空格,生成新的 sqlBuffer 对象
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
// <2> 全部大写
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
// <3> 应用 TrimSqlNode 的 trim 逻辑
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;
// prefixesToOverride 非空,先删除
if (prefixesToOverride != null) {
for (String toRemove : prefixesToOverride) {
if (trimmedUppercaseSql.startsWith(toRemove)) {
sql.delete(0, toRemove.trim().length());
break;
}
}
}
// prefix 非空,再添加
if (prefix != null) {
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
} private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
if (!suffixApplied) {
suffixApplied = true;
// suffixesToOverride 非空,先删除
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;
}
}
}
// suffix 非空,再添加
if (suffix != null) {
sql.append(" ");
sql.append(suffix);
}
}
}
}

逻辑并不复杂,大家可以看下

WhereSqlNode

org.apache.ibatis.scripting.xmltags.WhereSqlNode:继承了TrimSqlNode类,<where /> 标签对应的 SqlNode 实现类,代码如下:

public class WhereSqlNode extends TrimSqlNode {

    /**
* 也是通过 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);
}
}

基于TrimSqlNode类,定义了需要添加的前缀为WHERE和需要删除的前缀ANDOR

SetSqlNode

org.apache.ibatis.scripting.xmltags.SetSqlNode:继承了TrimSqlNode类,<set /> 标签对应的 SqlNode 实现类,代码如下:

public class SetSqlNode extends TrimSqlNode {

  /**
* 也是通过 TrimSqlNode ,这里定义需要删除的前缀
*/
private static final List<String> COMMA = Collections.singletonList(","); public SetSqlNode(Configuration configuration,SqlNode contents) {
// 设置前缀、需要删除的前缀和后缀
super(configuration, contents, "SET", COMMA, null, COMMA);
} }

基于TrimSqlNode类,定义了需要添加的前缀为SET、需要删除的前缀,和需要删除的后缀,

ForeachNode

org.apache.ibatis.scripting.xmltags.ForeachNode:实现 SqlNode 接口,<foreach /> 标签对应的 SqlNode 实现类

其中apply(DynamicContext context)方法的处理逻辑饶了我半天,大家可以仔细看一下

构造方法
public class ForEachSqlNode implements SqlNode {
/**
* 集合中元素绑定到上下文中 key 的前缀
*/
public static final String ITEM_PREFIX = "__frch_";
/**
* 表达式计算器
*/
private final ExpressionEvaluator evaluator;
/**
* 需要遍历的集合类型,支持:list set map array
*/
private final String collectionExpression;
/**
* MixedSqlNode,包含该<where />节点内所有信息
*/
private final SqlNode contents;
/**
* 开头
*/
private final String open;
/**
* 结尾
*/
private final String close;
/**
* 每个元素以什么分隔
*/
private final String separator;
/**
* 集合中每个元素的值
*/
private final String item;
/**
* 集合中每个元素的索引
*/
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;
}
}

对每个属性进行赋值,参考每个属性上面的注释

apply方法
@Override
public boolean apply(DynamicContext context) {
// 获取入参
Map<String, Object> bindings = context.getBindings();
/*
* <1> 获得遍历的集合的 Iterable 对象,用于遍历
* 例如配置了 collection 为以下类型
* list:则从入参中获取到 List 集合类型的属性的值
* array:则从入参中获取到 Array 数组类型的属性的值,会转换成 ArrayList
* map:则从入参中获取到 Map 集合类型的属性的值
*/
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
// 集合中没有元素则无需遍历
return true;
}
boolean first = true;
// <2> 添加 open 到 SQL 中
applyOpen(context);
int i = 0;
for (Object o : iterable) {
// <3> 记录原始的 context 对象,下面通过两个装饰器对他进行操作
DynamicContext oldContext = context;
// <4> 生成一个 context 装饰器
if (first || separator == null) {
context = new PrefixedContext(context, "");
} else {
// 设置其需要添加的前缀为分隔符
context = new PrefixedContext(context, separator);
}
// <5> 生成一个唯一索引值
int uniqueNumber = context.getUniqueNumber();
// Issue #709
// <6> 绑定到 context 中
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
/*
* 和下面同理,只不过索引是 Map 的 key
*/
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
/*
* 绑定当前集合中当前元素的索引到当前解析 SQL 语句的上下文中
*
* 1. 'index' -> i
*
* 2. __frch_'index'_uniqueNumber -> i
*/
applyIndex(context, i, uniqueNumber);
/*
* 绑定集合中当前元素的值到当前解析 SQL 语句的上下文中
*
* 1. 'item' -> o
*
* 2. __frch_'item'_uniqueNumber -> o
*
*/
applyItem(context, o, uniqueNumber);
}
/*
* 再装饰一下 PrefixedContext -> FilteredDynamicContext
*
* 前者进行前缀的添加,第一个元素添加后设置为已添加标记,后续不在添加
* 后者将<foreach />标签内的"#{item}"或者"#{index}"替换成上面我们已经绑定的数据:"#{__frch_'item'_uniqueNumber}"或者"#{__frch_'index'_uniqueNumber}"
*
* <7> 进行转换,将<foreach />标签内部定义的内容进行转换
*/
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) { // <8> 判断 prefix 是否已经插入
first = !((PrefixedContext) context).isPrefixApplied();
}
// <9> 恢复原始的 context 对象,因为目前 context 是装饰器
context = oldContext;
i++;
}
// <10> 添加 close 到 SQL 中
applyClose(context);
// <11> 移除 index 和 item 对应的绑定
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
} private void applyIndex(DynamicContext context, Object o, int i) {
if (index != null) {
context.bind(index, o);
context.bind(itemizeItem(index, i), o);
}
}
private void applyItem(DynamicContext context, Object o, int i) {
if (item != null) {
context.bind(item, o);
context.bind(itemizeItem(item, i), o);
}
}
private void applyOpen(DynamicContext context) {
if (open != null) {
context.appendSql(open);
}
}
private void applyClose(DynamicContext context) {
if (close != null) {
context.appendSql(close);
}
}
private static String itemizeItem(String item, int i) {
return ITEM_PREFIX + item + "_" + i;
}
  1. 获得需要遍历的集合 Iterable 对象,调用ExpressionEvaluatorevaluateIterable(String expression, Object parameterObject)方法,根据表达式从参数中获取集合对象

    1. 先通过OgnlCache根据Ognl表达式从上下文的ContextMap中获取转换后的结果,OgnlCache在后面会讲到
    2. 如果是Array数组类型,则转换成ArrayList集合后返回
    3. 如果是Map类型,则调用Map.Entry的集合
  2. 如果定义了open属性,则先拼接到SQL中

  3. 开始遍历集合 Iterable 对象,先记录context原始对象为oldContext,因为接下来需要对其进行两次装饰,而这里会再次进入

  4. 创建一个PrefixedContext对象,装饰context,主要是对集合中的每个元素添加separator分隔符

  5. 生成一个唯一索引值,也就是DynamicContext的uniqueNumber++,这样集合中每个元素都有一个唯一索引了

  6. 将集合中的当前元素绑定到上下文中,会保存以下信息:

    applyIndex:如果配置了index属性,则将当前元素的索引值绑定到上下文的ContextMap中,保存两个数据:

    1. 'index' -> i,其中'index'就是我们在<foreach />标签中配置的index属性,i就是当前元素在集合中的索引

    2. __frch_'index'_uniqueNumber -> i

    applyItem:如果配置了item属性,则将当前元素绑定到上下文的ContextMap中,保存两个数据:

    1. 'item' -> o,其中'item'就是我们在<foreach />标签中配置的item属性,o就是当前元素对象
    2. __frch_'item'_uniqueNumber -> o
  7. 再将PrefixedContext对象装饰成FilteredDynamicContext对象

    然后应用<foreach />标签内部的SqlNode节点们

    主要是替换我们在<foreach />标签中定义的内容,替换成上面第6步绑定的数据的key值,这样就可以获取到该key对应的value了

    例如:将<foreach />标签内的#{item}或者#{index}替换成第6步已经绑定的数据的key值#{__frch_'item'_uniqueNumber}或者#{__frch_'index'_uniqueNumber},然后拼接到SQL中

  8. 判断是否添加了open前缀,添加了则遍历时不用再添加前缀

  9. 恢复原始的oldContext对象,因为目前context是装饰器,然后继续遍历

  10. 如果定义了close属性,则拼接到SQL中

  11. 从上下文的ContextMap中移除第6步绑定的第1条数据

6步中,如果是Map类型,i对应的就是key值,o对应的就是value值,为什么两个方法都需要保存第1条数据?

因为<foreach />标签中可能还有其他的标签,例如<if />标签,它的判断条件中可能需要用到当前元素或者索引值,而表达式中使用了'index'或者'item',那么就需要从上下文中获取到对应的值了

那么接下来我们来看看内部定义的两个类:PrefixedContext和FilteredDynamicContext

PrefixedContext

ForeachNode的内部类,继承了DynamicContext,用于应用<foreach />标签时添加分隔符

重写了appendSql方法,逻辑比较简单,判断是否需要添加分隔符,代码如下:

private class PrefixedContext extends DynamicContext {
/**
* 装饰的 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);
} @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();
}
}
FilteredDynamicContext

ForeachNode的私有静态内部类,继承了DynamicContext,用于应用<foreach />标签时替换内部的#{item}或者#{index},

重写了appendSql方法,代码如下:

private static class FilteredDynamicContext extends DynamicContext {
/**
* 装饰的对象
*/
private final DynamicContext delegate;
/**
* 集合中当前元素的索引
*/
private final int index;
/**
* <foreach />定义的 index 属性
*/
private final String itemIndex;
/**
* <foreach />定义的 item 属性
*/
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();
} @Override
public void appendSql(String sql) {
GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
// 如果在`<foreach />`标签下的内容为通过item获取元素,则替换成`__frch_'item'_uniqueNumber`
String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
/*
* 如果在`<foreach />`标签中定义了index属性,并且标签下的内容为通过index获取元素
* 则替换成`__frch_'index'_uniqueNumber`
*/
if (itemIndex != null && newContent.equals(content)) {
newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
}
/*
* 返回`#{__frch_'item'_uniqueNumber}`或者`#{__frch_'index'_uniqueNumber}`
* 因为在前面已经将集合中的元素绑定在上下文的ContextMap中了,所以可以通过上面两个key获取到对应元素的值
* 例如绑定的数据:
* 1. __frch_'item'_uniqueNumber = 对应的元素值
* 2. __frch_'index'_uniqueNumber = 对应的元素值的索引
*/
return "#{" + newContent + "}";
}); delegate.appendSql(parser.parse(sql));
} @Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
}
  1. 创建一个GenericTokenParser对象parser,用于处理 #{}

  2. 创建一个TokenHandler处理器,大致的处理逻辑:

    1. 如果在<foreach />标签下的内容为通过item获取元素,则替换成__frch_'item'_uniqueNumber
    2. 如果在<foreach />标签中定义了index属性,并且标签下的内容为通过index获取元素,则替换成__frch_'index'_uniqueNumber
    3. 返回#{__frch_'item'_uniqueNumber}或者#{__frch_'index'_uniqueNumber},因为在前面已经将集合中的元素绑定在上下文的ContextMap中了,所以可以通过上面两个key获取到对应元素的值
  3. 调用parser进行解析,使用第2创建处理器进行处理,然后将转换后的结果拼接到SQL中

IfSqlNode

org.apache.ibatis.scripting.xmltags.IfSqlNode:实现 SqlNode 接口,<if /> 标签对应的 SqlNode 实现类,代码如下:

public class IfSqlNode implements SqlNode {
/**
* 表达式计算器
*/
private final ExpressionEvaluator evaluator;
/**
* 判断条件的表达式
*/
private final String test;
/**
* MixedSqlNode,包含该<if />节点内所有信息
*/
private final SqlNode contents; public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
} @Override
public boolean apply(DynamicContext context) {
// <1> 判断是否符合条件
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// <2> 解析该<if />节点中的内容
contents.apply(context);
return true;
}
// <3> 不符合
return false;
}
}
  1. 调用ExpressionEvaluatorevaluateBoolean(String expression, Object parameterObject)方法,根据表达式从参数中获取结果

    1. 先通过OgnlCache根据Ognl表达式从上下文的ContextMap中获取转换后的结果,OgnlCache在后面会讲到
    2. 如果是Boolean,则转换成Boolean类型返回
    3. 如果是Number类型,则判断是否不等于 0
    4. 其他类则判断是否不等于null
  2. 根据第1步的结果判断是否应用<if />标签内的SqlNode节点们

ChooseSqlNode

org.apache.ibatis.scripting.xmltags.ChooseSqlNode:实现 SqlNode 接口,<choose /> 标签对应的 SqlNode 实现类,代码如下:

public class ChooseSqlNode implements SqlNode {
/**
* <otherwise /> 标签对应的 SqlNode 节点
*/
private final SqlNode defaultSqlNode;
/**
* <when /> 标签对应的 SqlNode 节点数组
*/
private final List<SqlNode> ifSqlNodes; public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
this.ifSqlNodes = ifSqlNodes;
this.defaultSqlNode = defaultSqlNode;
} @Override
public boolean apply(DynamicContext context) {
// <1> 先判断 <when /> 标签中,是否有符合条件的节点。
// 如果有,则进行应用。并且只因应用一个 SqlNode 对象
for (SqlNode sqlNode : ifSqlNodes) {
if (sqlNode.apply(context)) {
return true;
}
}
// <2> 再判断 <otherwise /> 标签,是否存在
// 如果存在,则进行应用
if (defaultSqlNode != null) {
defaultSqlNode.apply(context);
return true;
}
// <3> 返回都失败
return false;
}
}
  1. 先应用<choose />下的所有<when />标签所对应的IfSqlNode,有一个应用成功则返回true
  2. 如果所有的<when />都不满足条件,则应用<otherwise />标签下的内容所对应的SqlNode

StaticTextSqlNode

org.apache.ibatis.scripting.xmltags.StaticTextSqlNode:实现 SqlNode 接口,用于保存静态文本,逻辑比较简单,直接拼接文本,代码如下:

public class StaticTextSqlNode implements SqlNode {
/**
* 静态内容
*/
private final String text; public StaticTextSqlNode(String text) {
this.text = text;
} @Override
public boolean apply(DynamicContext context) {
// 直接往正在解析 SQL 语句的上下文的 SQL 中添加该内容
context.appendSql(text);
return true;
}
}

TextSqlNode

org.apache.ibatis.scripting.xmltags.TextSqlNode:实现了 SqlNode 接口,用于处理${},注入对应的值,代码如下:

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;
} public boolean isDynamic() {
// <1> 创建 DynamicCheckerTokenParser 对象
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
// <2> 创建 GenericTokenParser 对象
GenericTokenParser parser = createParser(checker);
// <3> 执行解析,如果存在 '${ }',则 checker 会设置 isDynamic 为true
parser.parse(text);
// <4> 判断是否为动态文本
return checker.isDynamic();
} @Override
public boolean apply(DynamicContext context) {
// <1> 创建 BindingTokenParser 对象
// <2> 创建 GenericTokenParser 对象
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
// <3> 执行解析
// <4> 将解析的结果,添加到 context 中
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) {
// 从上下文中获取入参对象,在DynamicContext的构造方法中可以看到为什么可以获取到
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);
}
// 使用 OGNL 表达式,获得对应的值
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());
}
}
}
}

在XML文件中编写SQL语句时,如果使用到了${}作为变量时,那么会生成TextSqlNode对象,可以回看XMLScriptBuilderparseDynamicTags()方法

在MyBatis处理SQL语句时就会将${}进行替换成对应的参数,存在SQL注入的安全性问题

#{}就不一样了,MyBatis会将其替换成?占位符,通过java.sql.PreparedStatement进行预编译处理,不存在上面的问题

  1. 创建GenericTokenParser对象parser,用于处理${},设置的Token处理器为BindingTokenParser
  2. 执行解析,我们可以看到BindingTokenParser的handleToken(String content)方法
    1. 从上下文中获取入参对象,在DynamicContext的构造方法中可以看到为什么可以获取到
    2. 在将入参对象绑定到上下文中,设置key为"value",为什么这么做呢??没有仔细探究,可能跟OGNL相关,感兴趣的小伙伴可以探讨一下

      精尽 MyBatis 源码分析 - MyBatis 初始化(三)之 SQL 初始化(上)的更多相关文章

      1. 精尽MyBatis源码分析 - MyBatis初始化(四)之 SQL 初始化(下)

        该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

      2. 精尽 MyBatis 源码分析 - MyBatis 初始化(一)之加载 mybatis-config.xml

        该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

      3. 精尽MyBatis源码分析 - MyBatis初始化(二)之加载Mapper接口与XML映射文件

        该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

      4. MyBatis源码分析-MyBatis初始化流程

        MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...

      5. 精尽MyBatis源码分析 - MyBatis 的 SQL 执行过程(一)之 Executor

        该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

      6. mybatis源码分析--如何加载配置及初始化

        简介 Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository ...

      7. MyBatis源码分析(四):SQL执行过程分析

        一.获取Mapper接口的代理 根据上一节,Mybatis初始化之后,利用sqlSession(defaultSqlSession)的getMapper方法获取Mapper接口 1 @Override ...

      8. springboot集成mybatis源码分析-mybatis的mapper执行查询时的流程(三)

        例: package com.example.demo.service; import com.example.demo.dao.UserDao; import com.example.demo.do ...

      9. 精尽MyBatis源码分析 - 文章导读

        该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

      随机推荐

      1. JS逻辑运算符之“短路”

        逻辑与 && 如(表达式 1 && 表达式 2)参与运算时,程序只运行到对应的位置后,停止运行 二者都为 真(true) 时,返回表达式 2 二者有一个为 假 (fal ...

      2. oracle oracle sqldeveloper 12505 创建连接失败

        ref:http://blog.csdn.net/yangwenxue_admin/article/details/45062557

      3. 痞子衡嵌入式:基于恩智浦i.MXRT1010的MP3音乐播放器(RT-Mp3Player)设计

        大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是基于i.MXRT1011的MP3播放器参考设计. i.MXRT1011是恩智浦i.MXRT四位数系列的入门型号,虽然是入门级,可也是50 ...

      4. VMware Workstation Pro 虚拟机安装

        1.简介 虚拟机指通过软件莫比的具体有完整硬件系统功能的.运行在一个完全隔离环境中的完整计算机系统. 我们可以通过虚拟机软件,可以在一台物理计算机模拟出一台或多台虚拟的计算机,这些虚拟的计算机完全就像 ...

      5. A. Arena of Greed 解析(思維)

        Codeforce 1425 A. Arena of Greed 解析(思維) 今天我們來看看CF1425A 題目連結 題目 略,請直接看原題. 前言 明明是難度1400的題目,但總感覺不是很好寫阿, ...

      6. 【总结】spring aop

        1.aop简介 AOP的全称是Aspect Oriented Programming,面向切面编程.它的主要思想是在程序正常执行的某一个点切进去加入特定的逻辑.AOP框架中对AOP支持最完整的是Asp ...

      7. nacos 作为配置中心使用心得--配置使用

        1.页面配置 撇开原理不谈,先来介绍下nacos的基本使用,如下图nacos配置是以data id为单位进行使用的,基本上一个服务的一个配置文件就对应一个data id,支持的格式有xml,yaml, ...

      8. C# NModbus RTU通信实现

        Modbus协议时应用于电子控制器上的一种通用语言.通过此协议,控制器相互之间.控制器经由网络/串口和其它设备之间可以进行通信.它已经成为了一种工业标准.有了这个通信协议,不同的厂商生成的控制设备就可 ...

      9. Learn day8 re正则表达式\search函数\反射\tcp发送消息(循环)\udp发送消息

        1.匹配单个字符 # ### 正则表达式 - 单个字符匹配 import re ''' findall 把匹配的结果直接返回到列表中 lst = re.findall("正则表达式" ...

      10. NOIP2018 填坑记

        Oct,22ed,2018 DAY -18 又是颓废的一天呢 我好菜啊,一个圆方树弄了一整天(点双怎么那么毒瘤).(铁人两项怎么那么多点) Oct,23rd,2018 DAY -17 又双叒叕颓了一天 ...