11. Mybatis SQL解析源码分析

上文介绍了Mybatis SQL相关的三个核心类(MappedStatementSqlSourceBoundSql),但没有涉及到,这个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的方法为

  1. rootSqlNode.apply(context);:解析所有的动态SQL。
  2. 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结构如下的树形结构:

graph TD A[MixedSqlNode] --> B["StaticTextSqlNode
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));
      }
    }

处理器的工作流程是:

  1. 递归解析标签内的子节点,生成子SqlNode列表
  2. 提取标签属性(如<if>test属性)
  3. 创建对应SqlNode实现类的实例(如IfSqlNode
  4. 将创建的SqlNode添加到结果列表

不同的处理器大同小异,都是读取相应的属性,创建对应的SqlNode

三、SqlNode 如何根据参数生成最终SQL

文章的开头,我们已经看到,DynamicSqlSource通过SqlNodeapply方法,处理动态标签和${}占位符。接下来,我们就看下这个apply方法。

SqlNode.apply

前文已经提到过,SqlNode是一棵树,那么这个apply方法,必然是一个递归处理的过程,调用树上的各种节点的apply方法,以达到解析拼接SQL的目的。

(1)MixedSqlNode 混合SqlNode

我们先看下混合了各种SqlNodeMixedSqlNode:

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和一些解析后的参数,存储在上下文中,接下来就可以解析#{}了。

这里帮大家回忆一下DynamicSqlSourceSqlNodeapply执行完成后做了什么:

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());

这里使用了SqlSourceBuilderparse生成了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());
  }

接下来看下ParameterMappingTokenHandlerhandleToken做了什么:

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();

    }

回调做了两件事:

  1. 替换占位符为 ? :返回了?GenericTokenParser将所有#{}替换为?
  2. 生成参数映射列表 :将解析出的参数信息封装为ParameterMapping对象,加入到parameterMappings中,为后续的创建StaticSqlSource提供准备。

GenericTokenParser代码也挺值得看的,如果你也有字符串token解析的需求,可以参考一下。

总结

MyBatis 的动态 SQL 解析是一个精妙的分层设计:

  1. 解析阶段XMLScriptBuilder将 XML 中的动态标签递归解析为SqlNode树,每个节点对应特定的 SQL 片段或逻辑,这一过程在应用启动时完成。
  2. 执行阶段
    1. DynamicSqlSource触发SqlNode树的apply方法,根据运行时参数动态拼接 SQL。不同类型的SqlNode实现了各自的逻辑(条件判断、循环等),通过DynamicContext协作生成完整 SQL。
    2. SqlSourceBuilder.parse解析#{} 占位符被替换为 JDBC 的?,最终生成StaticSqlSource
    3. StaticSqlSource创建BoundSql

Mybatis在解析XML和生成SQL的逻辑中,都采用了策略模式,为不同的标签提供了不同的解析器(NodeHandler)和执行器(SqlNode)。很好的践行了单一职责原则,也有利于动态标签的扩展。

相关推荐
计算机编程小咖32 分钟前
《基于大数据的农产品交易数据分析与可视化系统》选题不当,毕业答辩可能直接挂科
java·大数据·hadoop·python·数据挖掘·数据分析·spark
艾莉丝努力练剑33 分钟前
【C语言16天强化训练】从基础入门到进阶:Day 7
java·c语言·学习·算法
老华带你飞1 小时前
校园交友|基于SprinBoot+vue的校园交友网站(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·校园交友网站
自强的小白1 小时前
学习Java24天
java·学习
Ashlee_code2 小时前
香港券商櫃台系統跨境金融研究
java·python·科技·金融·架构·系统架构·区块链
还梦呦2 小时前
2025年09月计算机二级Java选择题每日一练——第五期
java·开发语言·计算机二级
2501_924890523 小时前
商超场景徘徊识别误报率↓79%!陌讯多模态时序融合算法落地优化
java·大数据·人工智能·深度学习·算法·目标检测·计算机视觉
從南走到北3 小时前
JAVA国际版东郊到家同城按摩服务美容美发私教到店服务系统源码支持Android+IOS+H5
android·java·开发语言·ios·微信·微信小程序·小程序
毅航4 小时前
从原理到实践,讲透 MyBatis 内部池化思想的核心逻辑
后端·面试·mybatis
qianmoq4 小时前
第04章:数字流专题:IntStream让数学计算更简单
java