已经写了9篇Mybatis的源码分析文章了,之前分析了很多,但似乎一直没有讲解过,Mybatis是如何把XML的SQL配置,转换为真正执行的SQL的,本文就简单的探讨下,XML中的SQL配置,是如何转换为最终的SQL的。
一、概述
MyBatis对XML中SQL 的解析始于应用启动阶段,由SqlSessionFactory
初始化触发,最终将 XML 中的每一条SQL脚本转化为可执行的对象链。以下流程图清晰展示了从XML脚本到SQL的转换之旅:
如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
关键流程说明:
- XML 读取与解析 :MyBatis 通过
XMLMapperBuilder
扫描 XML Mapper 文件,定位 select、insert 等 SQL 节点; - SqlSource 构建 :根据SQL是否含动态标签,创建对应的
SqlSource
实现类; - MappedStatement 封装 :将 SQL 的元数据(ID、参数类型、返回类型、SqlSource 等)封装为
MappedStatement
; - 全局配置存储 :所有
MappedStatement
存入Configuration
,形成全局可访问的 SQL 元数据池; - 执行阶段转化 :当调用Mapper方法时,
SqlSource
生成BoundSql
,包含含有?
占位符的SQL + 参数映射,供后续执行做准备。
XML脚本转换后的对象:
- 解析阶段 : XML ->
SqlSource
->MappedStatement
- 执行阶段 :
MappedStatement
调用SqlSource
->BoundSql
从这个流程就可以看出来,SQL处理的核心是:
MappedStatement
: SQL的元数据信息,包括id
、SqlSource
、parameterType
、resultMap
等属性。SqlSource
: 提供动态SQL能力的核心接口。BoundSql
: 根据SQL生成的包含含有?
占位符的SQL + 参数映射的对象。
下面我们梳理下这三个类。
二、MappedStatement:XML SQL的"元数据总容器"
MappedStatement
是XML中单条SQL脚本解析后的核心产物,它封装了该SQL的所有元数据信息,是 MyBatis执行SQL的 "说明书"。每一条 XML中的SQL节点都会对应一个MappedStatement
对象,并存入Configuration
的mappedStatements
(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的"动态生成引擎"
SqlSource
是MappedStatement
的核心组成部分,负责将 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
(如DynamicSqlSource
、RawSqlSource
)解析后的 "终点"。
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;
}
}
解析过程:
- 通过
SqlNode.apply()
处理所有动态标签(如<if test="id != null">
会根据参数判断是否拼接 SQL 片段),生成包含#{}
的中间 SQL。 - 再通过
SqlSourceBuilder
将#{}
转换为?
,生成StaticSqlSource
。 - 最终通过
StaticSqlSource
生成BoundSql
。
特点:
- 支持动态逻辑,每次执行时都会重新解析(因参数可能影响动态标签的结果)。
- 内部依赖
SqlNode
体系(MyBatis 动态 SQL 的核心,如IfSqlNode
、ForEachSqlNode
等)。
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字符串,再委托给 DynamicSqlSource
或 RawSqlSource
处理。
源码核心逻辑:
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);
}
}
}
解析过程:
- 反射调用 Provider 类的指定方法(如
UserSqlProvider.selectByCondition()
)生成 SQL 字符串。 - 判断生成的 SQL 是否含动态内容(
${}
或动态标签):- 若是,创建
DynamicSqlSource
处理; - 若否,创建
RawSqlSource
处理。
- 若是,创建
- 最终通过委托的
SqlSource
生成BoundSql
。
特点:
- 桥接注解与XML式SQL处理,本质是将注解生成的SQL字符串转换为其他
SqlSource
处理。 - 支持动态生成 SQL(通过 Provider 方法的逻辑)。
Q:诸如
@Select
的注解的SQL是如何解析的?
A:也是根据是否含动态内容(${}
或动态标签),创建DynamicSqlSource
或RawSqlSource
支持
3. SqlSource的关系与协作流程
- 继承关系 :四者均直接实现
SqlSource
接口,是 "兄弟" 关系。 - 协作关系 :
ProviderSqlSource
是 "前置处理器",负责从注解生成 SQL 字符串,再委托给DynamicSqlSource
或RawSqlSource
。DynamicSqlSource
和RawSqlSource
是 "中间处理器":RawSqlSource
处理静态 SQL,初始化时直接生成StaticSqlSource
;DynamicSqlSource
处理动态 SQL,每次执行时动态生成StaticSqlSource
。
StaticSqlSource
是 "最终载体",所有 SQL 最终都会通过它生成BoundSql
。
- 使用场景流转:
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执行的 "最终蓝图"
BoundSql
是SqlSource
调用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. 核心作用
BoundSql
为StatementHandler
提供关键支持:
- 创建 PreparedStatement:使用
getSql
调用connection.prepareStatement(sql)
; - 绑定参数:
ParameterHandler
通过parameterMappings
和parameterObject
完成参数绑定; - 结果映射:辅助
ResultSetHandler
将结果集转化为 Java 对象。
五、总结
最后用一张类图做总结