MyBatis-Plus 源码阅读(一)CRUD 代码自动生成原理深度剖析

环境

  • 核心依赖: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,就能直接调用 selectByIdinsertupdateById 等通用方法。这背后究竟是怎样的实现逻辑?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 步:

  1. 读取 SQL 模板(来自 SqlMethod 枚举)
  2. TableInfo 元信息替换模板占位符,生成 SqlSource
  3. SqlSource 封装为 MappedStatement 并注册到 MyBatis

三、核心拆解:SQL 生成的两大关键组件

1. SqlMethod:CRUD 方法与 SQL 模板的映射中心

SqlMethod 是 MP 定义的枚举类,包含了所有 BaseMapper 通用方法的元信息,核心是 SQL 模板

打开源码就能发现,BaseMapper 的每一个方法(如 selectByIdinsertupdateById)都对应一个枚举项,且内置了标准化的 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
  • 主键信息(keyColumnkeyPropertyidType
  • 字段列表(fieldList
  • 逻辑删除配置(logicDeleteFieldlogicDeleteValue
  • 字段与数据库列的映射关系(如 deleteTimestampdelete_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 拼接流程:

  1. 原始模板(来自 SqlMethod.SELECT_BY_ID):SELECT %s FROM %s WHERE %s=#{%s} %s

  2. 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()
  3. 最终生成的 SQL:SELECT id,name,password,date,delete_timestamp FROM user WHERE id=#{id} AND delete_timestamp=0

四、核心流程总结:CRUD 自动生成的完整链路

结合前文拆解,MP 实现 CRUD 自动生成的完整流程可概括为 3 个阶段:

阶段 1:启动时初始化(TableInfo 缓存)

  1. Spring Boot 启动,MP 自动配置类 MybatisPlusAutoConfiguration 生效
  2. MP 扫描所有继承 BaseMapper 的 Mapper 接口(如 UserMapper
  3. 对每个 Mapper 对应的实体类(如 User),通过 TableInfoHelper.initTableInfo 解析注解和全局配置,生成 TableInfo 并缓存

阶段 2:SQL 注入(MappedStatement 注册)

  1. MP 的注入器 MybatisPlusInjector 遍历所有 AbstractMethod 实现类(如 SelectByIdInsert 等)

  2. 对每个 AbstractMethod,调用 injectMappedStatement 方法:

    • 读取 SqlMethod 中的 SQL 模板
    • TableInfo 填充模板占位符,生成 SqlSource
    • SqlSource 封装为 MappedStatement 并注册到 MyBatis 配置

阶段 3:运行时执行

  1. 开发者调用 userMapper.selectById(1)
  2. MyBatis 从配置中获取对应的 MappedStatement
  3. 解析 SqlSource 生成最终可执行的 SQL,执行并返回结果

五、结语

本文通过 selectById 方法,拆解了 MP CRUD 自动生成的核心逻辑:

  • SqlMethod 定义了标准化的 SQL 模板,是 CRUD 方法的「蓝图」
  • TableInfo 封装了表结构元信息,是 SQL 生成的「数据源」
  • injectMappedStatement 是 SQL 注入的核心入口,负责将模板与元信息结合,生成 MappedStatement

MP 的设计精髓在于:通过「注解解析 + 元信息缓存 + SQL 模板」的组合,在不改变 MyBatis 原有逻辑的前提下,实现了 CRUD 代码的自动化生成,既保证了灵活性,又极大提升了开发效率。

相关推荐
狂奔小菜鸡6 小时前
Day7 | Java的流程控制详解
java·后端·编程语言
霸道流氓气质6 小时前
Java中使用Collator实现对象List按照中文姓名属性进行A-Z的排序实现
java·开发语言·list
我命由我123456 小时前
Android PDF 操作 - AndroidPdfViewer 弹出框显示 PDF
android·java·java-ee·pdf·android studio·android-studio·android runtime
2501_938791226 小时前
服务器恶意进程排查:从 top 命令定位到病毒文件删除的实战步骤
java·linux·服务器
不见长安在6 小时前
HashMap的源码学习
java·hashmap
星光一影6 小时前
基于Jdk17+SpringBoot3AI智慧教育平台,告别低效学习,AI精准导学 + 新架构稳跑
java·学习·mysql
SimonKing6 小时前
Spring Boot全局异常处理的背后的故事
java·后端·程序员
ac.char6 小时前
编辑 JAR 包内嵌套的 TXT 文件(Vim 操作)
java·pycharm·vim·jar
我想进大厂6 小时前
Mybatis中# 和 $的区别
java·sql·tomcat