接上篇MyBatis-Plus 源码阅读(二)代码生成器原理深度剖析,本文我们聚焦 MP 中高频使用的条件构造器(Wrapper) ,从 SQL 配置、源码结构到执行流程,逐层拆解其工作原理。
环境
- 核心依赖:mybatis-plus-spring-boot3-starter 3.5.14
- 基础框架:Spring Boot 3.5.6
- JDK 版本:17
- 开发工具:IntelliJ IDEA
Wrapper 是什么?
先回顾官方定义:Wrapper 是 MP 提供的条件构造工具类,支持链式调用构造 SQL 查询条件,无需手动拼接字符串,既提升开发效率,又能规避 SQL 注入风险。
举个直观例子:
java
QueryWrapper<User> qw = new QueryWrapper<User>()
.ge("id", 3)
.eq("name", "happy")
.last("limit 1");
userMapper.selectList(qw);
最终生成的 SQL:
sql
SELECT
id,name,password,date,delete_timestamp
FROM user
WHERE (id >= 3 AND name = 'happy')
limit 1
相比传统 MyBatis 需要编写 XML / 注解 SQL,Wrapper 实现了动态条件的 "无感知" 构建------ 这背后的逻辑正是本文要拆解的重点。
源码分析:从 SQL 配置到条件构造
定位 selectList 的 SQL 模板
userMapper.selectList(qw) 最终调用的是 BaseMapper#selectList(Wrapper<T>),而该方法的 SQL 模板来自 MP 的内置注入逻辑。(参考MyBatis-Plus 源码阅读(一)CRUD 方法自动生成原理深度剖析)
在 SelectList#injectMappedStatement 方法中,我们能看到核心 SQL 配置。 
下面是 SQL 模板格式化后的内容。
xml
<script>
<if test="ew != null and ew.sqlFirst != null">${ew.sqlFirst}</if>
SELECT
<choose>
<when test="ew != null and ew.sqlSelect != null">${ew.sqlSelect}</when>
<otherwise>
id,
name,
password,
date,
delete_timestamp
</otherwise>
</choose>
FROM user
<if test="ew != null">
<bind name="_sgEs_" value="ew.sqlSegment != null and ew.sqlSegment != ''" />
<where>
<if test="ew.entity != null">
<if test="ew.entity.id != null">id=#{ew.entity.id}</if>
<if test="ew.entity['name'] != null">AND name=#{ew.entity.name}</if>
<if test="ew.entity['password'] != null">AND password=#{ew.entity.password}</if>
<if test="ew.entity['date'] != null">AND date=#{ew.entity.date}</if>
<if test="ew.entity['deleteTimestamp'] != null">AND delete_timestamp=#{ew.entity.deleteTimestamp}</if>
</if>
<if test="_sgEs_ and ew.nonEmptyOfNormal">AND ${ew.sqlSegment}</if>
</where>
<if test="_sgEs_ and ew.emptyOfNormal">${ew.sqlSegment}</if>
</if>
<if test="ew != null and ew.sqlComment != null">${ew.sqlComment}</if>
</script>
关键观察:
ew是 Wrapper 对象在 SQL 模板中的别名(通过@Param("ew")注入);- 第 25 行,
${ew.sqlSegment}是 Wrapper 构造的条件核心 ------我们写的 ge/eq 等方法,最终都会转化为 sqlSegment 的内容。
补充说明:
_sgEs_:判断 sqlSegment 是否非空;ew.nonEmptyOfNormal:标记是否包含普通查询条件(如 ge/eq);
Wrapper 代码分析
下面是 Wrapper 类简单的继承关系
AbstractWrapper 的泛型设计值得关注:
T:实体类类型(如User);R:数据库列名的表达类型(QueryWrapper 为String,LambdaQueryWrapper 为SFunction);Children:子类自身类型(在本例中为QueryWrapper<User>)。
本文以 QueryWrapper 为例分析(LambdaQueryWrapper 仅字段解析逻辑不同,核心原理一致)。
构造条件时发生了什么?
第一个条件 ge
以 .ge("id", 3) 为例,发现会进入com.baomidou.mybatisplus.core.conditions.AbstractWrapper#ge方法。最后将 3 个 ISqlSegment 的实现类添加到 AbstractWrapper 类的 expression 变量里。
experssion 的类型为 MergeSegments。
java
public class MergeSegments implements ISqlSegment {
private final NormalSegmentList normal = new NormalSegmentList();
private final GroupBySegmentList groupBy = new GroupBySegmentList();
private final HavingSegmentList having = new HavingSegmentList();
private final OrderBySegmentList orderBy = new OrderBySegmentList();
// ...
像 ge、eq 这样的条件都是添加到 NormalSegmentList 里面的。
那 ISqlSegment 又是什么?
java
@FunctionalInterface
public interface ISqlSegment extends Serializable {
/**
* SQL 片段
*/
String getSqlSegment();
}
其实就是 mp 定义的一个函数式接口,用于返回 sql 片段。
在上面的代码中6,.ge("id", 3)这个代码添加的三个 ISqlSegment 的内容分别为 id, >= 和 #{ew.paramNameValuePairs.MPGENVAL1}
注意,第三个 ISqlSegment 返回的内容不是 3 而是一个预编译的变量 ew.paramNameValuePairs.MPGENVAL1,并且在生成这个 ISqlSegment 的时候会调用 com.baomidou.mybatisplus.core.conditions.AbstractWrapper#formatParam 方法,将实际的参数值存入 paramNameValuePairs 变量。在上面例子中,参数名为 MPGENVAL1,值为 3。
第二个条件 eq
当调用 .eq("name", "happy") 时,MP 会自动在两个条件间添加 AND 连接符.
因此,.ge().eq() 最终生成的片段序列为:id → >= → #{...MPGENVAL1} → AND → name → = → #{...MPGENVAL2}
第三个条件 last
.last("limit 1") 更简单 ------ 直接将字符串赋值给 AbstractWrapper 的 lastSql 属性,最终在 SQL 模板末尾拼接。
sqlSegment 的最终生成
当所有的条件构造完成之后,我们构造的 queryWrapper 通过 getSqlSegment() 方法获取到的内容应该就是
sql
(id >= #{ew.paramNameValuePairs.MPGENVAL1}
AND
name = #{ew.paramNameValuePairs.MPGENVAL2})
limit 1
SQL 模板与条件的最终融合
Wrapper 构造的条件最终要与 SQL 模板结合,这一步由 MyBatis 的动态 SQL 引擎完成(核心在 DynamicSqlSource#getBoundSql)
java
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 1. 将 ${} 的内容转换为实际内容,
// 例如 ${ew.sqlSegment} 转换为 (id >= #{ew.paramNameValuePairs.MPGENVAL1} AND name = #{ew.paramNameValuePairs.MPGENVAL2}) limit 1
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 2. 处理 #{} 这样的预编译内容
// 例如 (id >= #{ew.paramNameValuePairs.MPGENVAL1} AND name = #{ew.paramNameValuePairs.MPGENVAL2}) limit 1 转换为 (id >=? AND name = ?) limit 1
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
总结
通过分析,我们理清了 Wrapper 的核心逻辑:以链式调用收集条件片段 → 存储为预编译占位符形式 → 拼接为 sqlSegment → 与 MyBatis 动态 SQL 融合生成最终 SQL
除了本文分析的基础条件构造,Wrapper 还支持:
select("id", "name"):通过sqlSelect指定查询字段;orderByAsc("id"):通过orderBy容器构造排序条件;lambda():LambdaQueryWrapper 通过解析User::getId获取字段名,避免硬编码。
这些功能的底层逻辑与本文分析的条件构造一致 ------ 都是通过 ISqlSegment 片段容器和动态拼接实现。
结语
Wrapper 本质是 MP 为开发者封装的 "动态 SQL 构建 DSL",它将复杂的条件拼接逻辑隐藏在链式调用背后,既简化了代码,又保证了安全性。理解其原理后,无论是自定义条件构造器,还是排查复杂条件的拼接问题,都会更加得心应手。
如果这篇文章对你理解「条件构造器原理」有帮助,麻烦点赞 + 关注支持一下~ 你的认可就是我持续输出源码解析系列的最大动力!❤️❤️❤️🤩🤩🤩🚀🚀🚀