上文介绍了Mybatis SQL相关的三个核心类(MappedStatement
、SqlSource
、BoundSql
),但没有涉及到,这个SQL到底是如何解析的,以及最终的SQL是如何生成的。本文就进一步解析一下,XML是如何解析的,SQL最终又是如何生成的。
一、核心组件:SqlNode接口
我们回顾下上一章中最复杂的DynamicSqlSource
:
java
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
// 根节点:封装了所有动态 SQL 节点(如 IfSqlNode、ForEachSqlNode 等)
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 1. 创建参数上下文(封装参数对象)
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 2. 解析所有动态节点,生成仅剩余#{}占位符的SQL
rootSqlNode.apply(context);
// 3. 用 SqlSourceBuilder 解析 #{} 为 ?,生成 StaticSqlSource
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 4. 委托给 StaticSqlSource 获取 BoundSql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 5. 绑定动态上下文的额外参数(如 <bind> 标签定义的变量)
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
我们可以看到,核心解析动态SQL的方法为
rootSqlNode.apply(context);
:解析所有的动态SQL。sqlSourceParser.parse
:解析#{}
为?
,这段代码在RawSqlSource
中也能看到。
可以说SqlNode
是 MyBatis 处理动态SQL的核心接口,它代表了SQL语句中的一个片段,可能是静态文本,也可能是包含逻辑判断的动态片段。也可以是嵌套多个SqlNode
的一棵树。所有动态标签(如<if>
、<foreach>
)最终都会被解析为SqlNode
的实现类。
1. SqlNode 接口定义
最新 MyBatis 源码中,SqlNode
接口仅有一个核心方法:
java
public interface SqlNode {
/**
* 应用当前SQL节点到上下文,实现SQL片段的拼接
* @param context 动态SQL上下文,存储拼接中的SQL和参数
* @return 是否应用成功
*/
boolean apply(DynamicContext context);
}
apply
方法的作用是将当前SqlNode
代表的 SQL 片段应用到DynamicContext
中,实现 SQL 的动态拼接。DynamicContext
本质上是一个容器,保存了正在构建的 SQL 字符串和相关参数信息。
2. 主要 SqlNode 实现类
MyBatis 为不同类型的SQL片段提供了对应的SqlNode
实现:
实现类 | 对应标签 / 场景 | 功能描述 |
---|---|---|
StaticTextSqlNode |
静态文本 | 处理不含任何动态元素的纯文本 SQL 片段 |
TextSqlNode |
含${} 的文本 |
处理包含${} 占位符的文本,支持字符串替换 |
IfSqlNode |
<if> 标签 |
根据test 属性的条件判断是否拼接其子 SQL 片段 |
ChooseSqlNode |
<choose> 标签 |
类似 Java 的switch 语句,仅执行第一个满足条件的<when> 子节点 |
WhenSqlNode |
<when> 标签 |
ChooseSqlNode 的子节点,包含条件判断 |
OtherwiseSqlNode |
<otherwise> 标签 |
ChooseSqlNode 的默认节点,当所有<when> 不满足时执行 |
ForEachSqlNode |
<foreach> 标签 |
遍历集合参数,循环拼接子 SQL 片段 |
WhereSqlNode |
<where> 标签 |
自动处理子片段中的AND/OR 前缀,确保生成正确的WHERE 子句 |
SetSqlNode |
<set> 标签 |
处理更新语句的SET 子句,自动移除多余的逗号 |
TrimSqlNode |
<trim> 标签 |
通用的文本修剪器,可自定义前缀、后缀及需要移除的前缀 后缀字符 |
MixedSqlNode |
组合节点 | 包含多个SqlNode 的集合,用于组合复杂的 SQL 片段 |
3. 实例:中等复杂 SQL 的 SqlNode 结构
考虑一个包含多种动态标签的查询 SQL:
xml
<select id="findUsers" parameterType="UserQuery">
SELECT id, username, email, age
FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="roles != null and roles.size() > 0">
AND role_id IN
<foreach collection="roles" item="roleId" open="(" close=")" separator=",">
#{roleId}
</foreach>
</if>
</where>
<if test="sortBy != null">
ORDER BY ${sortBy}
<if test="sortDir != null">
${sortDir}
</if>
</if>
</select>
这个 SQL 包含了<where>
、<if>
、<foreach>
等标签,以及#{}
和${}
两种占位符,其解析后生成的SqlNode
结构如下的树形结构:
SELECT id, username, email, age FROM user "] A --> C[WhereSqlNode] C --> D[MixedSqlNode] D --> E[IfSqlNode
test: username != null] E --> F["StaticTextSqlNode
AND username LIKE CONCAT('%', #{username}, '%')"] D --> G[IfSqlNode
test: age != null] G --> H["StaticTextSqlNode
AND age = #{age}"] D --> I[IfSqlNode
test: roles != null] I --> J[MixedSqlNode] J --> K["StaticTextSqlNode
AND role_id IN "] J --> L[ForEachSqlNode
collection: roles] L --> M["StaticTextSqlNode
#{roleId}"] A --> N[IfSqlNode
test: sortBy != null] N --> O[MixedSqlNode] O --> P["TextSqlNode
ORDER BY ${sortBy} "] O --> Q[IfSqlNode
test: sortDir != null] Q --> R["TextSqlNode
${sortDir}"]
二、XML 到 SqlNode 的解析过程
上文我们介绍了SqlNode
接口,接下来,我们看下MyBatis如何XML中的SQL标签解析为SqlNode
树的。该过程主要由XMLScriptBuilder
类完成。这个过程发生在应用启动时,属于MyBatis初始化的一部分。
1. 解析入口:XMLScriptBuilder.parseScriptNode ()
java
public class XMLScriptBuilder {
private final Configuration configuration;
private final XNode context;
private final Class<?> parameterType;
private boolean isDynamic;
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
//...
}
parseScriptNode
是解析的入口方法,他把解析后的MixedSqlNode
树,依据是否含有动态标签以及${}
占位符,创建DynamicSqlSource
或者RawSqlSource
。
解析过程依赖parseDynamicTags
方法递归解析XML节点,生成SqlNode
列表,最后用MixedSqlNode
包装作为根节点。
2. 核心解析逻辑:parseDynamicTags ()
parseDynamicTags
方法负责递归解析 XML 节点,将其转换为对应的SqlNode
对象:
java
private List<SqlNode> parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE
|| child.getNode().getNodeType() == Node.TEXT_NODE) {
// 处理文本节点
String text = child.getStringBody("");
if (text.trim().length() > 0) {
TextSqlNode textSqlNode = new TextSqlNode(text);
// 判断是否包含${}占位符
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(text));
}
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
// 获取Handler处理动态标签
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 contents;
}
该方法的处理逻辑可分为两类:
- 文本节点 :区分静态文本(
StaticTextSqlNode
)和含${}
的动态文本(TextSqlNode
) - 元素节点 :找到匹配的
NodeHandler
解析具体的动态标签
3. 动态标签解析:NodeHandler
MyBatis采用策略模式,使用不同的NodeHandler
实现,处理不同的动态标签,每个标签对应一个处理器:
java
// 节点处理器映射
private final Map<String, NodeHandler> nodeHandlers = new HashMap<>();
{
nodeHandlers.put("if", new IfHandler());
nodeHandlers.put("choose", new ChooseHandler());
nodeHandlers.put("when", new WhenHandler());
nodeHandlers.put("otherwise", new OtherwiseHandler());
nodeHandlers.put("foreach", new ForEachHandler());
nodeHandlers.put("trim", new TrimHandler());
nodeHandlers.put("where", new WhereHandler());
nodeHandlers.put("set", new SetHandler());
// ...其他处理器
}
以<if>
标签的处理器IfHandler
为例:
java
private class IfHandler implements NodeHandler {
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 递归解析子节点
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
String test = nodeToHandle.getStringAttribute("test");
// 创建IfSqlNode并添加到结果列表
targetContents.add(new IfSqlNode(contents, test));
}
}
处理器的工作流程是:
- 递归解析标签内的子节点,生成子
SqlNode
列表 - 提取标签属性(如
<if>
的test
属性) - 创建对应
SqlNode
实现类的实例(如IfSqlNode
) - 将创建的
SqlNode
添加到结果列表
不同的处理器大同小异,都是读取相应的属性,创建对应的SqlNode
。
三、SqlNode 如何根据参数生成最终SQL
文章的开头,我们已经看到,DynamicSqlSource
通过SqlNode
的apply
方法,处理动态标签和${}
占位符。接下来,我们就看下这个apply
方法。
SqlNode.apply
前文已经提到过,SqlNode
是一棵树,那么这个apply
方法,必然是一个递归处理的过程,调用树上的各种节点的apply
方法,以达到解析拼接SQL的目的。
(1)MixedSqlNode 混合SqlNode
我们先看下混合了各种SqlNode
的MixedSqlNode
:
java
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;
}
}
果然非常简单,就是遍历执行所有的SqlNode
。
(2)IfSqlNode 的条件判断
我们再以IfSqlNode
为例,看下其他SqlNode
的处理逻辑:
java
public class IfSqlNode implements SqlNode {
private final List<SqlNode> contents;
private final String test;
private final ExpressionEvaluator evaluator;
@Override
public boolean apply(DynamicContext context) {
// 评估test表达式
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 条件满足,应用所有子节点
contents.forEach(node -> node.apply(context));
return true;
}
return false;
}
}
IfSqlNode
使用ExpressionEvaluator
评估test
表达式(基于 OGNL 表达式引擎),当条件为true
时才拼接其子 SQL 片段。还有些其他的SqlNode
,都分别完成了自己的动态SQL的逻辑,这里不一一介绍了,感兴趣可以自行对源代码进行解读。
四、SqlSourceBuilder.parse
经过了SqlNode
树的解析,动态SQL标签和${}
占位符已经解析完成,生成了只剩下#{}
参数的SQL和一些解析后的参数,存储在上下文中,接下来就可以解析#{}
了。
这里帮大家回忆一下DynamicSqlSource
在SqlNode
的apply
执行完成后做了什么:
java
// 3. 用 SqlSourceBuilder 解析 #{} 为 ?,生成 StaticSqlSource
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
这里使用了SqlSourceBuilder
的parse
生成了StaticSqlSource
(和上一篇文章连起来了)。那么接下来,我们梳理下SqlSourceBuilder.parse
做了什么
java
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 1. 创建参数映射处理器
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// 2. 解析器,解析#{}占位符
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
// 3. 执行解析,遇到#{},回调ParameterMappingTokenHandler的handleToken方法
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
接下来看下ParameterMappingTokenHandler
的handleToken
做了什么:
java
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
private ParameterMapping buildParameterMapping(String content) {
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");
//省略了构建ParameterMapping的代码,还挺复杂的,有兴趣可以自行看源码
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
//省略了一堆代码
return builder.build();
}
回调做了两件事:
- 替换占位符为
?
:返回了?
,GenericTokenParser
将所有#{}
替换为?
- 生成参数映射列表 :将解析出的参数信息封装为
ParameterMapping
对象,加入到parameterMappings
中,为后续的创建StaticSqlSource
提供准备。
GenericTokenParser
代码也挺值得看的,如果你也有字符串token解析的需求,可以参考一下。
总结
MyBatis 的动态 SQL 解析是一个精妙的分层设计:
- 解析阶段 :
XMLScriptBuilder
将 XML 中的动态标签递归解析为SqlNode
树,每个节点对应特定的 SQL 片段或逻辑,这一过程在应用启动时完成。 - 执行阶段 :
DynamicSqlSource
触发SqlNode
树的apply
方法,根据运行时参数动态拼接 SQL。不同类型的SqlNode
实现了各自的逻辑(条件判断、循环等),通过DynamicContext
协作生成完整 SQL。SqlSourceBuilder.parse
解析#{}
占位符被替换为 JDBC 的?
,最终生成StaticSqlSource
StaticSqlSource
创建BoundSql
。
Mybatis在解析XML和生成SQL的逻辑中,都采用了策略模式,为不同的标签提供了不同的解析器(NodeHandler
)和执行器(SqlNode
)。很好的践行了单一职责原则,也有利于动态标签的扩展。