10. Mybatis XML配置到SQL的转换之旅

已经写了9篇Mybatis的源码分析文章了,之前分析了很多,但似乎一直没有讲解过,Mybatis是如何把XML的SQL配置,转换为真正执行的SQL的,本文就简单的探讨下,XML中的SQL配置,是如何转换为最终的SQL的。

一、概述

MyBatis对XML中SQL 的解析始于应用启动阶段,由SqlSessionFactory初始化触发,最终将 XML 中的每一条SQL脚本转化为可执行的对象链。以下流程图清晰展示了从XML脚本到SQL的转换之旅:

flowchart TD subgraph 执行阶段 G[调用Mapper接口方法
如userMapper.getUserById] --> H[从Configuration获取
对应MappedStatement] H --> I[Executor通过MappedStatement获取BoundSql
含最终SQL+参数映射] I --> J[Executor使用MappedStatement+BoundSql+参数+分页生成缓存key
管理一级&二级缓存] J --> K[StatementHandler使用BoundSql创建Statement] K --> L[ParameterHandler使用BoundSql的参数映射,设置参数] L --> M[执行SQL并处理结果
ResultSetHandler映射为Java对象] end subgraph 解析阶段 A[XML Mapper文件
如UserMapper.xml] --> B[XML解析器
XMLMapperBuilder] B --> C[解析SQL节点
select/insert/update/delete] C --> D[构建SqlSource对象
静态/动态SQL适配] D --> E[封装MappedStatement
SQL元数据容器] E --> F[存入Configuration全局配置
MyBatis核心配置中心] end

关键流程说明

  1. XML 读取与解析 :MyBatis 通过XMLMapperBuilder扫描 XML Mapper 文件,定位 select、insert 等 SQL 节点;
  2. SqlSource 构建 :根据SQL是否含动态标签,创建对应的SqlSource实现类;
  3. MappedStatement 封装 :将 SQL 的元数据(ID、参数类型、返回类型、SqlSource 等)封装为MappedStatement
  4. 全局配置存储 :所有MappedStatement存入Configuration,形成全局可访问的 SQL 元数据池;
  5. 执行阶段转化 :当调用Mapper方法时,SqlSource生成BoundSql,包含含有?占位符的SQL + 参数映射,供后续执行做准备。

XML脚本转换后的对象

  1. 解析阶段 : XML -> SqlSource -> MappedStatement
  2. 执行阶段 : MappedStatement调用SqlSource -> BoundSql

从这个流程就可以看出来,SQL处理的核心是:

  • MappedStatement : SQL的元数据信息,包括idSqlSourceparameterTyperesultMap等属性。
  • SqlSource: 提供动态SQL能力的核心接口。
  • BoundSql : 根据SQL生成的包含含有?占位符的SQL + 参数映射的对象。

下面我们梳理下这三个类。

二、MappedStatement:XML SQL的"元数据总容器"

MappedStatement是XML中单条SQL脚本解析后的核心产物,它封装了该SQL的所有元数据信息,是 MyBatis执行SQL的 "说明书"。每一条 XML中的SQL节点都会对应一个MappedStatement对象,并存入ConfigurationmappedStatements(Map 结构)中,key 为 "namespace+SQL id"(一般就是Mapper的全类名+方法名)。

MappedStatement 的核心属性与作用

属性名 类型 作用
id String SQL的唯一标识,"namespace + 方法名",一般是Mapper的全类名+方法名
sqlSource SqlSource 存储 SQL 的核心对象,负责生成最终可执行 SQL
parameterMap ParameterMap 可选,参数映射配置,定义参数名与JDBC类型的对应关系,很少使用
resultMap ResultMap 可选,结果集映射配置,定义数据库字段与Java对象属性的映射规则
parameterType Class<?> SQL参数的Java类型
resultType Class<?> SQL返回结果的Java类型,与resultMap二选一
statementType StatementType 执行SQL的JDBC语句类型,默认PREPARED,对应PreparedStatement
cache Cache 该SQL对应的缓存配置
timeout Integer SQL 执行超时时间,单位为秒

核心作用

MappedStatement是MyBatis解析XML的最终产物,一个<select><update>、<insert>对应一个MappedStatement。执行Mapper方法时,MyBatis先根据 "mapper全类名 + 方法名" 从Configuration中找到对应的MappedStatement,再基于其元数据完成 SQL 生成、参数绑定、结果映射等后续操作。

三、SqlSource:SQL的"动态生成引擎"

SqlSourceMappedStatement的核心组成部分,负责将 XML 中的 SQL 脚本转化为最终可执行的 SQL 语句,处理动态 SQL 的拼接逻辑。它是一个接口,MyBatis 根据 SQL 类型提供 4 种核心实现类,适配不同场景需求。

1. 核心接口定义

java 复制代码
public interface SqlSource {
    // 根据传入的参数对象,生成BoundSql
    BoundSql getBoundSql(Object parameterObject);
}

2. 四大实现类与适用场景

StaticSqlSource:静态SQL载体

作用 :持有最终可直接执行的静态 SQL(已处理完所有动态逻辑和占位符,SQL 中仅含 ?),是其他SqlSource解析后的最终产物。

源码核心逻辑

java 复制代码
    public class StaticSqlSource implements SqlSource {
      // 最终可执行的 SQL(含 ? 占位符)
      private final String sql;
      // 参数与 SQL 占位符的映射关系
      private final List<ParameterMapping> parameterMappings;
      // 配置信息
      private final Configuration configuration;

      public StaticSqlSource(Configuration configuration, String sql) {
        this(configuration, sql, null);
      }

      public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
        this.sql = sql;
        this.parameterMappings = parameterMappings;
        this.configuration = configuration;
      }

      @Override
      public BoundSql getBoundSql(Object parameterObject) {
        // 直接用持有的 SQL 和参数映射创建 BoundSql
        return new BoundSql(configuration, sql, parameterMappings, parameterObject);
      }
    }

特点

  • 无动态逻辑,SQL 固定不变,直接返回 BoundSql
  • 是其他 SqlSource(如 DynamicSqlSourceRawSqlSource)解析后的 "终点"。

RawSqlSource:预解析的静态SQL处理器

作用 :处理无动态标签 的静态 SQL(仅含 #{} 占位符,不含 ${}<if>/<foreach> 等动态标签)。它会在初始化时提前解析 SQL,将 #{} 转换为 ? 并生成 StaticSqlSource,避免重复解析,提升性能。

源码核心逻辑

java 复制代码
    public class RawSqlSource implements SqlSource {
      private final SqlSource sqlSource;

      // 构造时直接解析 SQL,生成 StaticSqlSource
      public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
        // 调用 SqlSourceBuilder 解析 SQL(#{} -> ?)
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> clazz = parameterType == null ? Object.class : parameterType;
        // 解析后得到 StaticSqlSource
        this.sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
      }

      @Override
      public BoundSql getBoundSql(Object parameterObject) {
        // 直接委托给内部的 StaticSqlSource
        return sqlSource.getBoundSql(parameterObject);
      }
    }

解析过程

通过 SqlSourceBuilder#{} 占位符替换为 ?,并生成 ParameterMapping 列表,最终封装为 StaticSqlSource。例如:

举例

原始SQL:select * from user where id = #{id}

解析后SQL:select * from user where id = ?(由 StaticSqlSource 持有)。

特点

  • 仅处理静态 SQL(无动态标签),初始化时完成解析,后续调用直接复用 StaticSqlSource
  • 性能优于 DynamicSqlSource(避免每次执行都解析)。

DynamicSqlSource:动态SQL处理器

作用 :处理含动态标签 的 SQL(如 <if><foreach><where> 等)或 ${} 占位符(文本替换)。它会在每次执行时(调用 getBoundSql)动态解析 SQL,生成 StaticSqlSource

源码核心逻辑

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;
      }
    }

解析过程

  1. 通过 SqlNode.apply() 处理所有动态标签(如 <if test="id != null"> 会根据参数判断是否拼接 SQL 片段),生成包含 #{} 的中间 SQL。
  2. 再通过 SqlSourceBuilder#{} 转换为 ?,生成 StaticSqlSource
  3. 最终通过 StaticSqlSource 生成 BoundSql

特点

  • 支持动态逻辑,每次执行时都会重新解析(因参数可能影响动态标签的结果)。
  • 内部依赖 SqlNode 体系(MyBatis 动态 SQL 的核心,如 IfSqlNodeForEachSqlNode 等)。

ProviderSqlSource:注解SQL处理器

应用举例 : 当SQL逻辑复杂(如需要大量条件判断、动态拼接),用 XML 动态标签(<if>/<foreach>)实现不够灵活时,可通过Java类的方法生成SQL。

java 复制代码
public interface UserMapper {
    // 通过@SelectProvider指定SQL提供类和方法
    @SelectProvider(type = UserSqlProvider.class, method = "selectByCondition")
    List<User> selectByCondition(UserQuery query);
}
public class UserSqlProvider {
    // 动态生成查询SQL
    public String selectByCondition(UserQuery query) {
        StringBuilder sql = new StringBuilder("SELECT * FROM user WHERE 1=1");
        if (query.getName() != null) {
            sql.append(" AND name = #{name}");
        }
        if (query.getAge() != null) {
            sql.append(" AND age = #{age}");
        }
        return sql.toString();
    }
}

作用 :以上这种通过@SelectProvider(还有@InsertProvider等)定义的 SQL。它会通过反射调用 Provider类的方法生成SQL字符串,再委托给 DynamicSqlSourceRawSqlSource 处理。

源码核心逻辑

java 复制代码
    public class ProviderSqlSource implements SqlSource {
      private final SqlSource sqlSource;

      public ProviderSqlSource(Configuration configuration, Class<?> providerType, String providerMethodName) {
        // 1. 解析 Provider 注解,获取 SQL 生成器(ProviderMethodResolver)
        ProviderMethodResolver resolver = getProviderMethodResolver(providerType);
        // 2. 确定要调用的 Provider 方法(生成 SQL 的方法)
        Method providerMethod = resolver.resolveMethod(providerMethodName);
        // 3. 调用 Provider 方法生成 SQL 字符串
        String sql = generateSql(providerMethod, ...);
        // 4. 根据生成的 SQL 是否含动态标签,选择委托给 DynamicSqlSource 或 RawSqlSource
        this.sqlSource = createSqlSource(configuration, sql, parameterType);
      }

      @Override
      public BoundSql getBoundSql(Object parameterObject) {
        // 委托给内部的 SqlSource(DynamicSqlSource 或 RawSqlSource)
        return sqlSource.getBoundSql(parameterObject);
      }

      // 根据 SQL 是否含动态内容,创建对应的 SqlSource
      private SqlSource createSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
        if (containsDynamicSql(sql)) { // 含 ${} 或动态标签
          return new DynamicSqlSource(configuration, parseSqlNode(configuration, sql));
        } else { // 纯静态 SQL
          return new RawSqlSource(configuration, sql, parameterType);
        }
      }
    }

解析过程

  1. 反射调用 Provider 类的指定方法(如 UserSqlProvider.selectByCondition())生成 SQL 字符串。
  2. 判断生成的 SQL 是否含动态内容(${} 或动态标签):
    • 若是,创建 DynamicSqlSource 处理;
    • 若否,创建 RawSqlSource 处理。
  3. 最终通过委托的 SqlSource 生成 BoundSql

特点

  • 桥接注解与XML式SQL处理,本质是将注解生成的SQL字符串转换为其他 SqlSource 处理。
  • 支持动态生成 SQL(通过 Provider 方法的逻辑)。

Q:诸如@Select的注解的SQL是如何解析的?
A:也是根据是否含动态内容(${} 或动态标签),创建DynamicSqlSourceRawSqlSource支持

3. SqlSource的关系与协作流程

  1. 继承关系 :四者均直接实现 SqlSource 接口,是 "兄弟" 关系。
  2. 协作关系
    • ProviderSqlSource 是 "前置处理器",负责从注解生成 SQL 字符串,再委托给 DynamicSqlSourceRawSqlSource
    • DynamicSqlSourceRawSqlSource 是 "中间处理器":
      • RawSqlSource 处理静态 SQL,初始化时直接生成 StaticSqlSource
      • DynamicSqlSource 处理动态 SQL,每次执行时动态生成 StaticSqlSource
    • StaticSqlSource 是 "最终载体",所有 SQL 最终都会通过它生成 BoundSql
  3. 使用场景流转
sql 复制代码
注解 SQL(@SelectProvider)→ ProviderSqlSource → 
  动态 SQL → DynamicSqlSource → StaticSqlSource → BoundSql  
  静态 SQL → RawSqlSource → StaticSqlSource → BoundSql  
XML 静态 SQL → RawSqlSource → StaticSqlSource → BoundSql  
XML 动态 SQL → DynamicSqlSource → StaticSqlSource → BoundSql  

4、小结

SqlSource 类型 核心作用 处理场景 性能特点
StaticSqlSource 持有最终可执行的静态 SQL 所有 SQL 的最终形式 无解析成本,直接返回
RawSqlSource 预解析静态 SQL(仅含 #{} XML / 注解中的静态 SQL 初始化时解析,复用高效
DynamicSqlSource 动态解析含动态标签 /${} 的 SQL XML / 注解中的动态 SQL 每次执行都解析,灵活但低效
ProviderSqlSource 从注解生成 SQL 并委托给其他 SqlSource 注解定义的 SQL(如 @SelectProvider 依赖反射,本质是桥接器

这四种实现共同构成了 MyBatis 灵活的 SQL 处理体系,既支持静态 SQL 的高效执行,也支持动态 SQL 的灵活拼接,同时兼容XML和注解两种配置方式。

四、BoundSql:SQL执行的 "最终蓝图"

BoundSqlSqlSource调用getBoundSql后生成的对象,包含SQL 执行所需的所有 "实时信息",是 MyBatis与JDBC 交互的直接依据。

BoundSql 的核心属性

java 复制代码
public class BoundSql {
    private final String sql; // 最终可执行的SQL(含?占位符)
    private final List<ParameterMapping> parameterMappings; // 参数映射列表
    private final Object parameterObject; // 传入的参数对象
    private final Map<String, Object> additionalParameters; // 额外参数,比如<bind>标签引入的参数,再比如<foreach>标签引入的临时参数
    private final MetaObject metaObject; // 参数对象的元数据(反射获取属性值)
}

核心属性解析

(1)sql:最终可执行的 SQL

  • 静态 SQL:解析后的带?占位符 SQL;
  • 动态 SQL:执行时根据参数拼接后的完整 SQL。

(2)parameterMappings:参数绑定的 "导航图"

  • 每个ParameterMapping对应一个 #{} 占位符,包含参数名、JDBC 类型、类型处理器等信息;
  • ParameterHandler根据它将参数值绑定到 PreparedStatement 的?占位符上。

(3)parameterObject 与 additionalParameters:参数的 "数据源"

  • parameterObject:直接传入的参数(如整数、对象);
  • additionalParameters:存储参数对象的嵌套属性或临时参数(比如<bind>标签引入的参数,再比如<foreach>标签引入的临时参数)。

3. 核心作用

BoundSqlStatementHandler提供关键支持:

  1. 创建 PreparedStatement:使用getSql调用connection.prepareStatement(sql)
  2. 绑定参数:ParameterHandler通过parameterMappingsparameterObject完成参数绑定;
  3. 结果映射:辅助ResultSetHandler将结果集转化为 Java 对象。

五、总结

最后用一张类图做总结

classDiagram direction TB %% 核心接口 class SqlSource { <> + getBoundSql(parameterObject: Object): BoundSql } %% 4个实现类 class StaticSqlSource { - sql: String - parameterMappings: List~ParameterMapping~ - configuration: Configuration + StaticSqlSource(configuration: Configuration, sql: String) + StaticSqlSource(configuration: Configuration, sql: String, parameterMappings: List~ParameterMapping~) + getBoundSql(parameterObject: Object): BoundSql } class RawSqlSource { - sqlSource: SqlSource + RawSqlSource(configuration: Configuration, sql: String, parameterType: Class~?~) + getBoundSql(parameterObject: Object): BoundSql } class DynamicSqlSource { - configuration: Configuration - rootSqlNode: SqlNode + DynamicSqlSource(configuration: Configuration, rootSqlNode: SqlNode) + getBoundSql(parameterObject: Object): BoundSql } class ProviderSqlSource { - sqlSource: SqlSource + ProviderSqlSource(configuration: Configuration, providerType: Class~?~, providerMethodName: String) + getBoundSql(parameterObject: Object): BoundSql - createSqlSource(configuration: Configuration, sql: String, parameterType: Class~?~): SqlSource - generateSql(providerMethod: Method, ...): String } %% 关键依赖类 class BoundSql { - sql: String - parameterMappings: List - parameterObject: Object - additionalParameters: Map + BoundSql(configuration: Configuration, sql: String, parameterMappings: List, parameterObject: Object) + setAdditionalParameter(name: String, value: Object): void } class SqlNode { <> + apply(context: DynamicContext): boolean } class DynamicContext { - sqlBuilder: StringBuilder - bindings: Map + getSql(): String + getBindings(): Map } class Configuration { %% 简化表示,实际包含MyBatis核心配置 } class ParameterMapping { %% 简化表示,封装参数与SQL占位符的映射信息 } %% 继承关系:4个类均实现SqlSource接口 SqlSource <|-- StaticSqlSource SqlSource <|-- RawSqlSource SqlSource <|-- DynamicSqlSource SqlSource <|-- ProviderSqlSource %% 依赖关系:体现协作逻辑 StaticSqlSource --> BoundSql : 生成 StaticSqlSource --> Configuration : 依赖(构造参数) StaticSqlSource --> ParameterMapping : 依赖(参数映射列表) RawSqlSource --> SqlSource : 委托(内部持有,实际为StaticSqlSource) RawSqlSource --> Configuration : 依赖(构造参数) DynamicSqlSource --> SqlNode : 依赖(根节点,处理动态标签) DynamicSqlSource --> DynamicContext : 依赖(解析动态SQL上下文) DynamicSqlSource --> SqlSource : 生成(每次执行创建StaticSqlSource) DynamicSqlSource --> Configuration : 依赖(构造参数) ProviderSqlSource --> SqlSource : 委托(内部持有,DynamicSqlSource/RawSqlSource) ProviderSqlSource --> Configuration : 依赖(构造参数) BoundSql --> ParameterMapping : 依赖(参数映射列表) BoundSql --> Configuration : 依赖(构造参数)
相关推荐
㳺三才人子6 小时前
初探 Flask
后端·python·flask·html
星栈独行6 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Lei活在当下6 小时前
先用起来,再理解,关于协程Coroutine应该知道的事
android·java·jvm
Java爱好狂.6 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易7 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
tongluowan0077 小时前
以ReentrantLock为例解释AQS的工作流程
java·模板方法模式·aqs·reentrantlock
装不满的克莱因瓶7 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
ltl8 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
身如柳絮随风扬8 小时前
Java 项目打包与部署完全指南:JAR vs WAR,从构建到运行
java·firefox·jar
云烟成雨TD8 小时前
Spring AI Alibaba 1.x 系列【62】时光旅行(Time-Travel)
java·人工智能·spring