环境
- 核心依赖:mybatis-plus-spring-boot3-starter 3.5.14
- 基础框架:Spring Boot 3.5.6
- JDK 版本:17
- 开发工具:IntelliJ IDEA + MyBatisX 插件
前言
相比于原生 MyBatis,MyBatis-Plus(下文简称 MP)最核心的增强之一便是 CRUD 代码自动生成能力。它不仅彻底解放了重复的 XML 编写工作,更通过统一的命名规范和 SQL 模板,保证了项目代码的一致性与规范性。
我们只需让 Mapper 接口继承 BaseMapper,无需编写任何 SQL 或 XML,就能直接调用 selectById、insert、updateById 等通用方法。这背后究竟是怎样的实现逻辑?MP 是如何悄悄帮我们生成对应的 MappedStatement 并注册到 MyBatis 中的?
本文将以 selectById 方法为例,通过 Debug 追踪 + 源码拆解,带你揭开 MP CRUD 自动生成的神秘面纱。
一、调试准备:搭建最小验证场景
1. 数据表设计
首先创建一个简单的用户表,用于后续调试验证:
sql
create table user
(
id int auto_increment
primary key,
name varchar(10) null comment '用户名',
password varchar(15) null comment '密码',
date datetime null comment '创建时间',
delete_timestamp bigint default 0 not null comment '逻辑删除时间戳(0表示未删除)'
);
2. 自动生成核心代码
使用 MyBatisX 插件生成实体类 User 和 Mapper 接口 UserMapper(无需手动编写 XML)
实体类(含 MP 核心注解)
java
@TableName(value = "user") // 映射数据库表名
@Data
public class User {
@TableId(type = IdType.AUTO) // 主键策略:自增
private Integer id;
private String name;
private String password;
private Date date;
private Long deleteTimestamp; // 对应数据库字段 delete_timestamp
}
Mapper 接口(仅需继承 BaseMapper)
java
// 无需编写任何方法,直接继承 BaseMapper 即可获得所有通用 CRUD
public interface UserMapper extends BaseMapper<User> {
}
3. 关键 Debug 发现
在业务代码中调用 userMapper.selectById(1),并在 MyBatis 核心方法处打断点:
java
// MyBatis 执行 SQL 的核心方法
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(
java.lang.String, java.lang.Object,
org.apache.ibatis.session.RowBounds,
org.apache.ibatis.session.ResultHandler
)
调试发现:当执行 selectById 时,configuration.getMappedStatement(statement) 能成功获取到对应的 MappedStatement,且 sqlSource 中已包含完整的 SQL 语句:SELECT id,name,password,date,delete_timestamp FROM user WHERE id=#{id}
显然,这个 MappedStatement 是 MP 自动生成并注册到 MyBatis 配置中的。那么问题来了:MP 是在何时、何地生成并注册的?
二、溯源关键:找到 MappedStatement 生成入口
MyBatis 中所有 MappedStatement 的创建,最终都会调用 MP 重写的 addMappedStatement 方法:
java
com.baomidou.mybatisplus.core.MybatisConfiguration#addMappedStatement
在该方法处打断点(启动时触发),通过「Reset Frame」回溯调用栈,最终定位到核心生成方法:
java
com.baomidou.mybatisplus.core.injector.methods.SelectById#injectMappedStatement
这正是 MP 为 selectById 方法注入 SQL 的核心类!其完整源码如下:
java
public class SelectById extends AbstractMethod {
// 无参构造:默认使用 SqlMethod.SELECT_BY_ID 定义的方法名
public SelectById() {
this(SqlMethod.SELECT_BY_ID.getMethod());
}
// 有参构造:支持自定义方法名
public SelectById(String name) {
super(name);
}
@Override
public MappedStatement injectMappedStatement(
Class<?> mapperClass, // Mapper 接口(如 UserMapper)
Class<?> modelClass, // 实体类(如 User)
TableInfo tableInfo // 表结构元信息
) {
// 1. 获取 SQL 方法枚举(包含 SQL 模板)
SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;
// 2. 生成 SqlSource(MyBatis 中封装 SQL 语句的核心对象)
SqlSource sqlSource = super.createSqlSource(
configuration,
// 拼接 SQL 模板:替换占位符为实际表名、字段名等
String.format(sqlMethod.getSql(),
sqlSelectColumns(tableInfo, false), // 查询字段(id,name,password...)
tableInfo.getTableName(), // 表名(user)
tableInfo.getKeyColumn(), // 主键列名(id)
tableInfo.getKeyProperty(), // 主键属性名(id)
tableInfo.getLogicDeleteSql(true, true) // 逻辑删除条件(如 delete_timestamp=0)
),
Object.class // 参数类型
);
// 3. 注册 MappedStatement 到 MyBatis 配置
return this.addSelectMappedStatementForTable(
mapperClass, methodName, sqlSource, tableInfo
);
}
}
从源码可以清晰看到,injectMappedStatement 是 MP 自动生成 CRUD 的核心入口,其逻辑可拆解为 3 步:
- 读取 SQL 模板(来自
SqlMethod枚举) - 用
TableInfo元信息替换模板占位符,生成SqlSource - 将
SqlSource封装为MappedStatement并注册到 MyBatis
三、核心拆解:SQL 生成的两大关键组件
1. SqlMethod:CRUD 方法与 SQL 模板的映射中心
SqlMethod 是 MP 定义的枚举类,包含了所有 BaseMapper 通用方法的元信息,核心是 SQL 模板。
打开源码就能发现,BaseMapper 的每一个方法(如 selectById、insert、updateById)都对应一个枚举项,且内置了标准化的 SQL 模板:
java
public enum SqlMethod {
/**
* 插入(选择字段)
*/
INSERT_ONE("insert", "插入一条数据(选择字段插入)",
"<script>\nINSERT INTO %s %s VALUES %s\n</script>"),
/**
* 根据 ID 查询
*/
SELECT_BY_ID("selectById", "根据ID 查询一条数据",
"SELECT %s FROM %s WHERE %s=#{%s} %s"), // 5个占位符
/**
* 根据 ID 删除
*/
DELETE_BY_ID("deleteById", "根据ID 删除一条数据",
"DELETE FROM %s WHERE %s=#{%s} %s"),
// 其他方法(updateById、selectList 等)...
}
枚举项的三个参数含义:
- 第一个参数:
methodName→ 对应BaseMapper的方法名(如selectById) - 第二个参数:
desc→ 方法描述 - 第三个参数:
sql→ SQL 模板(%s为占位符,后续由TableInfo填充)
2. TableInfo:表结构元信息的「数据源」
SQL 模板中的占位符(如 %s)能被替换为实际表名、字段名,核心依赖 TableInfo 类 ------ 它是 MP 解析实体类后生成的 表结构元信息缓存,包含:
- 表名(
tableName) - 主键信息(
keyColumn、keyProperty、idType) - 字段列表(
fieldList) - 逻辑删除配置(
logicDeleteField、logicDeleteValue) - 字段与数据库列的映射关系(如
deleteTimestamp→delete_timestamp)
TableInfo 的生成入口
通过源码追踪,TableInfo 的生成入口在 TableInfoHelper 工具类的 initTableInfo 方法:
java
public static synchronized TableInfo initTableInfo(
MapperBuilderAssistant builderAssistant, Class<?> clazz // clazz 为实体类(如 User)
) {
// 1. 先查缓存:避免重复解析
TableInfo targetTableInfo = TABLE_INFO_CACHE.get(clazz);
final Configuration configuration = builderAssistant.getConfiguration();
if (targetTableInfo != null) {
// 若配置不同,重新初始化
if (!targetTableInfo.getConfiguration().equals(configuration)) {
targetTableInfo = initTableInfo(configuration, builderAssistant.getCurrentNamespace(), clazz);
}
return targetTableInfo;
}
// 2. 缓存未命中,执行初始化
return initTableInfo(configuration, builderAssistant.getCurrentNamespace(), clazz);
}
TableInfo 的初始化流程
核心初始化逻辑在私有方法 initTableInfo 中,步骤如下:
java
private static synchronized TableInfo initTableInfo(Configuration configuration, String currentNamespace, Class<?> clazz) {
GlobalConfig globalConfig = GlobalConfigUtils.getGlobalConfig(configuration);
// 1. 创建空的 TableInfo 实例
PostInitTableInfoHandler postInitTableInfoHandler = globalConfig.getPostInitTableInfoHandler();
TableInfo tableInfo = postInitTableInfoHandler.creteTableInfo(configuration, clazz);
tableInfo.setCurrentNamespace(currentNamespace);
// 2. 初始化表名(解析 @TableName 注解 + 全局配置)
PropertySelector propertySelector = initTableName(clazz, globalConfig, tableInfo);
// 3. 初始化字段信息(解析 @TableId、@TableField 等注解)
initTableFields(configuration, clazz, globalConfig, tableInfo, propertySelector);
// 4. 自动构建 ResultMap(用于 MyBatis 结果集映射)
tableInfo.initResultMapIfNeed();
// 5. 后置处理(扩展点)
postInitTableInfoHandler.postTableInfo(tableInfo, configuration);
// 6. 缓存 TableInfo(避免重复解析,提升性能)
TABLE_INFO_CACHE.put(clazz, tableInfo);
TABLE_NAME_INFO_CACHE.put(tableInfo.getTableName(), tableInfo);
// 7. 初始化 Lambda 缓存(支持 Lambda 条件构造)
LambdaUtils.installCache(tableInfo);
return tableInfo;
}
简单来说,TableInfo 的本质是:MP 通过反射解析实体类上的 MP 注解(@TableName、@TableId 等)和全局配置,生成的表结构元信息缓存。后续生成 SQL 时,直接从缓存中获取信息,避免重复解析,提升性能。
3. 最终 SQL 生成示例
以 selectById 为例,完整的 SQL 拼接流程:
-
原始模板(来自
SqlMethod.SELECT_BY_ID):SELECT %s FROM %s WHERE %s=#{%s} %s -
用
TableInfo填充占位符:%s(查询字段)→id,name,password,date,delete_timestamp(由sqlSelectColumns方法生成)%s(表名)→user(来自tableInfo.getTableName())%s(主键列名)→id(来自tableInfo.getKeyColumn())%s(主键属性名)→id(来自tableInfo.getKeyProperty())%s(逻辑删除条件)→AND delete_timestamp=0(来自tableInfo.getLogicDeleteSql())
-
最终生成的 SQL:
SELECT id,name,password,date,delete_timestamp FROM user WHERE id=#{id} AND delete_timestamp=0
四、核心流程总结:CRUD 自动生成的完整链路
结合前文拆解,MP 实现 CRUD 自动生成的完整流程可概括为 3 个阶段:
阶段 1:启动时初始化(TableInfo 缓存)
- Spring Boot 启动,MP 自动配置类
MybatisPlusAutoConfiguration生效 - MP 扫描所有继承
BaseMapper的 Mapper 接口(如UserMapper) - 对每个 Mapper 对应的实体类(如
User),通过TableInfoHelper.initTableInfo解析注解和全局配置,生成TableInfo并缓存
阶段 2:SQL 注入(MappedStatement 注册)
-
MP 的注入器
MybatisPlusInjector遍历所有AbstractMethod实现类(如SelectById、Insert等) -
对每个
AbstractMethod,调用injectMappedStatement方法:- 读取
SqlMethod中的 SQL 模板 - 用
TableInfo填充模板占位符,生成SqlSource - 将
SqlSource封装为MappedStatement并注册到 MyBatis 配置
- 读取
阶段 3:运行时执行
- 开发者调用
userMapper.selectById(1) - MyBatis 从配置中获取对应的
MappedStatement - 解析
SqlSource生成最终可执行的 SQL,执行并返回结果
五、结语
本文通过 selectById 方法,拆解了 MP CRUD 自动生成的核心逻辑:
SqlMethod定义了标准化的 SQL 模板,是 CRUD 方法的「蓝图」TableInfo封装了表结构元信息,是 SQL 生成的「数据源」injectMappedStatement是 SQL 注入的核心入口,负责将模板与元信息结合,生成MappedStatement
MP 的设计精髓在于:通过「注解解析 + 元信息缓存 + SQL 模板」的组合,在不改变 MyBatis 原有逻辑的前提下,实现了 CRUD 代码的自动化生成,既保证了灵活性,又极大提升了开发效率。