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

2025-12-12 0 238

环境

  • 核心依赖:mybatisplusspring-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. 数据表设计

首先创建一个简单的用户表,用于后续调试验证:

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 核心注解)

@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)

// 无需编写任何方法,直接继承 BaseMapper 即可获得所有通用 CRUD
public interface UserMapper extends BaseMapper {

}

3. 关键 Debug 发现

在业务代码中调用 userMapper.selectById(1),并在 MyBatis 核心方法处打断点:

// 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}

MyBatis-Plus 源码阅读(一)CRUD 代码自动生成原理深度剖析
显然,这个 MappedStatement 是 MP 自动生成并注册到 MyBatis 配置中的。那么问题来了:MP 是在何时、何地生成并注册的?

二、溯源关键:找到 MappedStatement 生成入口

MyBatis 中所有 MappedStatement 的创建,最终都会调用 MP 重写的 addMappedStatement 方法:

com.baomidou.mybatisplus.core.MybatisConfiguration#addMappedStatement

在该方法处打断点(启动时触发),通过「Reset Frame」回溯调用栈,最终定位到核心生成方法:

com.baomidou.mybatisplus.core.injector.methods.SelectById#injectMappedStatement

这正是 MP 为 selectById 方法注入 SQL 的核心类!其完整源码如下:

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 模板:

public enum SqlMethod {
    /**
     * 插入(选择字段)
     */
    INSERT_ONE(\"insert\", \"插入一条数据(选择字段插入)\", 
        \"nINSERT INTO %s %s VALUES %sn\"),
    
    /**
     * 根据 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
  • 字段与数据库列的映射关系(如 deleteTimestamp → delete_timestamp

TableInfo 的生成入口

通过源码追踪,TableInfo 的生成入口在 TableInfoHelper 工具类的 initTableInfo 方法:

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 中,步骤如下:

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

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 编程相关 MyBatis-Plus 源码阅读(一)CRUD 代码自动生成原理深度剖析 https://www.zuozi.net/35775.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务