环境
- 核心依赖: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 代码的自动化生成,既保证了灵活性,又极大提升了开发效率。