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 : 依赖(构造参数)
相关推荐
Ashlee_code1 小时前
香港券商櫃台系統跨境金融研究
java·python·科技·金融·架构·系统架构·区块链
还梦呦1 小时前
2025年09月计算机二级Java选择题每日一练——第五期
java·开发语言·计算机二级
2501_924890521 小时前
商超场景徘徊识别误报率↓79%!陌讯多模态时序融合算法落地优化
java·大数据·人工智能·深度学习·算法·目标检测·计算机视觉
從南走到北2 小时前
JAVA国际版东郊到家同城按摩服务美容美发私教到店服务系统源码支持Android+IOS+H5
android·java·开发语言·ios·微信·微信小程序·小程序
毅航3 小时前
从原理到实践,讲透 MyBatis 内部池化思想的核心逻辑
后端·面试·mybatis
qianmoq3 小时前
第04章:数字流专题:IntStream让数学计算更简单
java
展信佳_daydayup3 小时前
02 基础篇-OpenHarmony 的编译工具
后端·面试·编译器
Always_Passion3 小时前
二、开发一个简单的MCP Server
后端
用户721522078773 小时前
基于LD_PRELOAD的命令行参数安全混淆技术
后端
笃行3503 小时前
开源大模型实战:GPT-OSS本地部署与全面测评
后端