MyBatis-Plus代码生成器实战:告别重复CRUD,高效生成全套基础代码 如果你的日常也是写各种后台管理系统那你一定感受过这种痛苦数据库新增了一张表接下来就是实体类、Mapper 接口、Mapper XML、Service 接口、ServiceImpl、Controller 这一整套增删改查代码翻来覆去全是复制粘贴。MyBatis-Plus 代码生成器就是专治这个烦心事的连上数据库配置几段参数几秒钟就能把一张表对应的所有基础代码全部生成出来。这篇实战笔记我打算把编号 44 的 MyBatis-Plus 代码生成器项目完整拆开从依赖配置、版本选型、实战用法到常见坑点都过一遍适合所有想把重复劳动降到最低的 Java 开发者。1. 项目背景为什么我要在生产里用代码生成器1.1 手工写 CRUD 的重复劳动到底有多大先给你算一笔账。一个常规的后台模块通常一张表就要对应六个文件实体类、Mapper 接口、Mapper XML、Service 接口、ServiceImpl 实现类、Controller。如果这张表有 20 个字段Controller 里要写分页查询、列表查询、按 ID 查询、新增、修改、删除Mapper XML 里还要把字段列表、条件拼接、结果映射写清楚。我在实际开发中手写过一整套最快也要四五十分钟加上调试和测试一张表两个小时很正常。如果一个项目里有二三十张表光生成这些“地基代码”就要耗费一名开发好几天时间而且这些代码高度雷同Bug 却一点不少。最典型的就是复制过去的字段漏改表 A 有 userId表 B 的字段也叫 userId 但类型是 String复制时没同步改掉编译报错还算好的如果类型刚好对上了但逻辑不对要等联调才发现。这种问题出现几次之后我就下定决心把代码生成器用起来。1.2 为什么是 MyBatis-Plus 而不是 MyBatis Generator很多人第一反应是用 MyBatis 官方那套 MyBatis Generator但我在项目中实际对比过之后还是选了 MyBatis-Plus 的代码生成器。MBG 生成的模型类和查询条件类非常多XxxExample 那种查询条件写法在我看来很冗余团队里新人也容易看晕。而 MyBatis-Plus 生成器配合它自带的 BaseMapper、IService、ServiceImpl 这些通用方法单表 CRUD 大部分情况连 XML 都不用写。更关键的一点是团队本身已经在用 MyBatis-Plus 的 LambdaQueryWrapper以及分页插件、逻辑删除这些能力。生成器生成的代码就和团队现有的代码风格完全一致不需要统一改写。而且 MyBatis-Plus 生成器支持自定义模板团队规范可以直接固化到模板里这一块后期价值非常大我在第六章会专门讲。2. 前置准备与依赖配置2.1 基础依赖与版本选择如果你的项目是 Maven 构建在 pom.xml 里加这三个依赖dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-generator/artifactId version3.5.3.1/version scopeprovided/scope /dependency dependency groupIdorg.apache.velocity/groupId artifactIdvelocity-engine-core/artifactId version2.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency第一个是生成器核心依赖第二个是模板引擎。生成器默认使用 Velocity 渲染模板如果你不加这个依赖运行时会直接报模板引擎相关的 ClassNotFound 异常。第三个是数据库驱动版本号根据你数据库类型调整MySQL 8 就用这个。这里我故意把生成器依赖的 scope 写成 provided避免它被打进生产环境的 jar 包。如果你只是临时用一次也可以不加 scope但跑完记得从正式依赖里删掉否则对最终产物是一个无意义的体积拖累。2.2 数据源连接与驱动坑点代码生成器的第一步就是连数据库读表结构所以连接串要提前准备好。我常用的 MySQL 连接串是这样String url jdbc:mysql://localhost:3306/demo ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8;这里三个参数你最好都带上。serverTimezone 不带高版本驱动经常会报时区错误useSSL 在本地开发环境没什么用不加会输出一堆 SSL 告警characterEncoding 不指定生成出来的表字段注释如果是中文容易出现乱码。实际使用中如果你的数据库部署在远程记得把 localhost:3306 换成实际地址并确保当前账号有读取 information_schema 的权限否则生成器会发现“有表但读不到任何字段”。还有一个容易踩的坑老版本驱动坐标是 mysql:mysql-connector-java8.0.31 之后新坐标改为 com.mysql:mysql-connector-j。如果你从网上抄的老配置文件可能遇到依赖下载失败或驱动类找不到。用上面的新坐标比较稳妥。2.3 项目最小工程结构生成器本质上是一个独立入口不需要依赖 Spring 容器所以你可以把它放在任意一个模块里单独运行。我比较推荐在工程根目录下建一个 generator 模块结构大概是这样my-project/ ├── generator/ │ └── src/main/java/ │ └── com/myapp/generator/GeneratorApplication.java ├── service/ │ └── src/main/java/ │ └── com/myapp/... └── pom.xml这样做的最大好处是生成器相关依赖不会污染业务工程。如果你嫌建模块麻烦把入口类放在 test 目录下也可以因为 test 依赖默认不会打包进产物。但千万别放在业务主目录里随手就删不掉等哪天 build 的时候把生成器的依赖带进去又是一堆麻烦。3. 老版与新版生成器AutoGenerator 与 FastAutoGenerator3.1 老版 AutoGenerator 的写法如果你翻到三年前的文章看到的几乎都是 AutoGenerator 的写法GlobalConfig gc new GlobalConfig(); gc.setOutputDir(System.getProperty(user.dir) /src/main/java); gc.setAuthor(某开发者); gc.setSwagger2(true); DataSourceConfig dsc new DataSourceConfig(); dsc.setUrl(url); dsc.setUsername(root); dsc.setPassword(123456); dsc.setDriverName(com.mysql.cj.jdbc.Driver); PackageConfig pc new PackageConfig(); pc.setParent(com.myapp); pc.setModuleName(order); StrategyConfig strategy new StrategyConfig(); strategy.setInclude(t_order_info); strategy.setTablePrefix(t_); AutoGenerator generator new AutoGenerator(); generator.setGlobalConfig(gc); generator.setDataSource(dsc); generator.setPackageConfig(pc); generator.setStrategy(strategy); generator.execute();老版写法的问题在于每个配置都要先 new 一个 Config 对象再用 setter 填参数代码顺序一不小心就乱套。很多人从网上抄到的代码前面定义了好几个配置到最后 execution 的时候才发现某个对象根本没 new编译就过不去。而且这种写法对新手不友好大家容易记混是 setUrl 还是 setUsername。3.2 FastAutoGenerator 链式写法为什么更好MyBatis-Plus 3.5.1 之后官方主推 FastAutoGenerator用链式调用把配置组织得清清楚楚FastAutoGenerator.create(url, root, 123456) .globalConfig(builder - { builder.author(某开发者) .outputDir(System.getProperty(user.dir) /src/main/java) .enableSwagger(); }) .packageConfig(builder - { builder.parent(com.myapp) .moduleName(order); }) .strategyConfig(builder - { builder.addInclude(t_order_info) .addTablePrefix(t_); }) .execute();这个写法的好处是一眼就能看出每个配置块的职责globalConfig 管全局packageConfig 管包路径strategyConfig 管表匹配和生成策略。如果你漏了哪个配置IDE 自动补全的时候也会提示你该往哪个 builder 里填。而且作者在源码里给每个 builder 方法都写了 JavaDoc鼠标悬停就能看到说明排查问题比翻老版源码方便得多。3.3 生成器的核心工作流程很多人用了几年生成器都没想过它内部是怎么工作的其实原理很简单四步通过 JDBC 连接数据库读取指定库的表结构、字段类型、字段注释、索引信息把这些信息封装成表元数据对象TableInfo将表元数据填充到模板引擎的上下文里例如 Velocity 的 Context渲染模板字符串按配置的路径写入 Java 文件、XML 文件。理解了这个流程你就会明白自定义模板为什么灵活所谓的生成器本质就是“从数据库元数据到代码文件的批量模板渲染”。模板里能拿到哪些变量取决于生成器源码里 TableInfo 暴露了哪些属性。你不需要背住每个模板变量名只需要在源码里找到 TableInfo 类看一眼 getter 方法剩下的就顺理成章了。4. 配置详解全局配置、包配置、策略配置4.1 全局配置里容易忽略的开关全局配置里除了 author 和 outputDir有几个开关我建议你重点关注。enableSwagger 会在实体类属性上生成 Swagger 注解。如果你的项目没有引入 Swagger 相关依赖这个开关别开否则编译期就要额外处理注解依赖。反过来如果你的团队接口文档就是靠 Knife4j 或 SpringDoc 维护的那开启之后就省去给每个属性手写 ApiModelProperty 的时间。outputDir 的路径是个容易出问题的点。用 System.getProperty(user.dir) 这种写法取的是程序运行目录。如果你把生成器入口放在独立 generator 模块里运行目录可能是生成器模块的目录这样生成文件就落到了模块下和期望的业务模块对不上。我自己的习惯是在 outputDir 里直接写业务模块的相对路径比如 service/src/main/java然后在 generator 模块里通过工作目录调整或者干脆写绝对路径。还有一个 dateType这玩意影响时间字段的实体类型。默认情况下生成器可能给你生成 java.util.Date但我现在几乎所有项目都用 LocalDateTime所以我会在全局配置里显式设置.globalConfig(builder - { builder.author(某开发者) .outputDir(service/src/main/java) .dateType(DateType.TIME_PACK) .commentDate(yyyy-MM-dd); })4.2 包配置决定项目结构包配置决定了生成出来的代码落到哪些包下面我用过最多的配置是.packageConfig(builder - { builder.parent(com.myapp) .moduleName(order) .entity(entity) .mapper(mapper) .service(service) .serviceImpl(service.impl) .controller(controller) .xml(mapper.xml); })parent 填的是业务模块的根包名moduleName 是子模块名。如果某个系统同时有 order 和 user 两个业务模块建议 moduleName 分开这样生成后的目录是 com/myapp/order 和 com/myapp/user相互不干扰。xml 这个配置项管的是 Mapper XML 的包名但要注意它默认输出到 Java 源码目录下的 mapper.xml 包而不是 resources 下的 mapper 目录。这个很多人会踩我在第六章再给解决办法。4.3 策略配置表前缀、ID 策略、字段填充与逻辑删除策略配置是生成器里最有讲究的一环。 addInclude 指定要生成哪些表可以一次传多张表addTablePrefix 指定表名前缀配置 t_ 之后表 t_order_info 生成的实体类就是 OrderInfo而不是 TOrderInfo。没配前缀的后果不用多说了那名字丑到看都不想多看一眼。在 strategy 里面controllerBuilder 和 entityBuilder 我是这样配的.strategyConfig(builder - { builder.addInclude(t_order_info) .addTablePrefix(t_) .entityBuilder() .enableLombok() .enableTableFieldAnnotation() .idType(IdType.ASSIGN_ID) .controllerBuilder() .enableRestStyle() .enableHyphenStyle(); })idType 用 IdType.ASSIGN_ID 意味着主键生成策略是雪花算法。雪花 ID 的好处是分布式环境下不需要依赖数据库自增能在多实例写入时不冲突。如果你的系统只是单库单表用数据库自增也没问题可以把 idType 换成 IdType.AUTO。但一旦未来要分库分表自增主键基本就要重做所以新项目我默认直接用 ASSIGN_ID。需要特别提醒的一点逻辑删除和乐观锁字段目前版本的生成器并不会自动帮你加上对应注解。比如表里有 deleted 字段生成出来的实体类上就是一个普通属性MyBatis-Plus 并不会自动把它识别为逻辑删除。我一般是在生成之后手动在字段上补 TableLogic 注解版本号字段补 Version 注解这个没有什么高深技巧就是别指望一步到位。5. 完整实操从一张订单表生成整套代码5.1 建表 SQL 与生成入口模拟一个最常见的订单表建表 SQL 如下CREATE TABLE t_order_info ( id bigint NOT NULL COMMENT 主键ID, order_no varchar(64) NOT NULL COMMENT 订单编号, user_id bigint NOT NULL COMMENT 用户ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint NOT NULL COMMENT 订单状态 1-待支付 2-已支付 3-已取消, create_time datetime NOT NULL COMMENT 创建时间, update_time datetime NOT NULL COMMENT 更新时间, deleted tinyint NOT NULL DEFAULT 0 COMMENT 逻辑删除标记, remark varchar(255) DEFAULT NULL COMMENT 备注, PRIMARY KEY (id) ) ENGINEInnoDB COMMENT订单信息表;然后写一个入口 main 方法import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.DateType; import com.baomidou.mybatisplus.annotation.IdType; public class GeneratorApplication { public static void main(String[] args) { String url jdbc:mysql://localhost:3306/demo ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; FastAutoGenerator.create(url, root, 123456) .globalConfig(builder - { builder.author(某开发者) .outputDir(service/src/main/java) .dateType(DateType.TIME_PACK) .commentDate(yyyy-MM-dd) .enableSwagger(); }) .packageConfig(builder - { builder.parent(com.myapp) .moduleName(order) .entity(entity) .mapper(mapper) .service(service) .serviceImpl(service.impl) .controller(controller); }) .strategyConfig(builder - { builder.addInclude(t_order_info) .addTablePrefix(t_) .entityBuilder() .enableLombok() .enableTableFieldAnnotation() .idType(IdType.ASSIGN_ID) .controllerBuilder() .enableRestStyle() .enableHyphenStyle(); }) .execute(); } }直接运行 main 方法控制台会打印数据源连接信息和生成日志等到出现生成完成的提示时代码就已经落盘了。这里我再强调一遍数据库账号密码千万不要写错我见过太多人运行生成器时把数据库账号写成了测试库账号生成的实体跟本地库表结构完全对不上排查半天才发现连错库了。5.2 生成结果结构解读生成结束之后你会得到这样一套代码结构service/src/main/java/com/myapp/order/ ├── controller/OrderInfoController.java ├── entity/OrderInfo.java ├── mapper/OrderInfoMapper.java ├── mapper/xml/OrderInfoMapper.xml ├── service/OrderInfoService.java └── service/impl/OrderInfoServiceImpl.java这些文件各自的作用我逐个说一下。OrderInfo 是实体类MyBatis-Plus 会自动在类上生成 TableName(t_order_info)表前缀被去掉了这个注解帮你把实体和表关联起来。每个字段都会转成驼峰命名比如 create_time 变成 createTime并且如果你开了 enableTableFieldAnnotation每个字段上还会带 TableField 注解用起来非常清楚。OrderInfoMapper 接口继承了 BaseMapper 这意味着单表插入、删除、按 ID 查询、条件查询这些最常用的方法你都不用自己写 SQL直接调父接口方法就行。OrderInfoService 继承了 IServiceOrderInfoServiceImpl 继承了 serviceImplOrderInfoMapper, OrderInfo 并实现 OrderInfoService这就是 MyBatis-Plus 推荐的 Service 层用法。最容易被忽略的是 OrderInfoMapper.xml。生成器默认会生成一个结果映射和基础字段列表但不会生成任何业务 SQL。很多新手看到 XML 是空的就以为生成失败其实这是正常的。大部分基础 CRUD 可以走 BaseMapper只有需要写复杂多表联查的时候你才需要在这个 XML 里补 SQL。Controller 默认生成的也只有类注解和空的类体。简单场景下你可以在 Controller 里直接注入 OrderInfoService用 Wrapper 实现查询。我在团队里更推荐把 Service 当业务门面Controller 只管参数接收和结果包装这样后期好维护。5.3 接入业务层与补齐运行配置代码生成完之后第一件事不是急着写业务而是把项目跑起来。先确认 application.yml 里有 MyBatis-Plus 的配置spring: datasource: url: jdbc:mysql://localhost:3306/demo?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 username: root password: 123456 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml global-config: db-config: id-type: assign_idmapper-locations 一定要写成 classpath*:mapper/**/*.xml 这种匹配方式否则你用自定义输出把 XML 放到了 resources/mapper 下启动时项目会发现找不到 Mapper 绑定。然后我建议配置 MyBatis-Plus 的分页插件这是后台管理系统最常用的能力Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }分页插件配好之后你才能在 Service 里直接用 Page 对象做分页查询。还有 createTime 和 updateTime 这两个字段生成器只是把它当普通字段并不会在插入和更新时自动赋值所以我会补一个 MetaObjectHandlerComponent public class AuditMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }然后回到实体类里在 createTime 和 updateTime 字段上补上TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;这样之后每做一次新增或更新这两个时间字段都会被自动填充不需要你在业务代码里手动赋值省很多事。6. 自定义模板让生成代码符合团队规范6.1 为什么要动模板默认模板只适合快速搭建骨架但真实团队的代码规范往往更严格。比如我所在的项目组Controller 所有接口必须返回统一的响应体 R默认生成控制器根本做不到再比如 Mapper XML 大家都约定放在 resources/mapper 目录下生成器默认却输出到源码目录。这些问题靠改生成器参数是解决不了的唯一的办法就是自定义模板。自定义模板的好处是把团队约定“写死”在模板里任何一个人运行生成器产出的代码都是完全一致的风格。新人加入后不需要去背规范文档代码长什么样模板就是什么样。6.2 自定义 Controller 模板做法先在 resources 下建一个 templates 目录放一个自己的 controller.java.vm 文件。我用 Velocity 写一个简化版的控制器模板package ${package.Controller}; import com.myapp.common.result.R; import ${package.Service}.${table.serviceName}; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; RestController RequestMapping(/${entity?uncap_first}) public class ${table.controllerName} { Resource private ${table.serviceName} ${table.serviceName?uncap_first}Service; GetMapping(/detail/{id}) public R detail(PathVariable Long id) { return R.ok(${table.serviceName?uncap_first}Service.getById(id)); } PostMapping(/save) public R save(RequestBody ${entity} entity) { return R.ok(${table.serviceName?uncap_first}Service.save(entity)); } PostMapping(/delete/{id}) public R delete(PathVariable Long id) { return R.ok(${table.serviceName?uncap_first}Service.removeById(id)); } }然后在生成器的模板引擎配置里指定使用你自己的模板目录。不同小版本的配置入口略有差异但思路是一样把默认 controller 模板覆盖为你自定义的文件。你可以直接用默认 templates 里的 controller.java.vm 复制一份出来改这样变量名和结构都对齐不会因为少了某个变量导致渲染报错。6.3 自定义输出路径把 XML 放到 resources/mapper如果你坚持要把 Mapper XML 生成到 resources 目录我推荐用生成器的 FileOutConfig 来做这是我在老版本上验证过的方案ListFileOutConfig focList new ArrayList(); focList.add(new FileOutConfig(/templates/mapper.xml.vm) { Override public String outputFile(TableInfo tableInfo) { return service/src/main/resources/mapper/ tableInfo.getEntityName() Mapper.xml; } }); AutoGenerator generator new AutoGenerator(); generator.setFileOutConfigList(focList);新版本 FastAutoGenerator 里也有类似能力只是不同版本的 builder 方法名会变建议你在使用前看一眼当前版本的源码或者直接用这个思路去查官方示例。只要把输出路径指对了生成出来的 XML 就会待在 resources/mapper 下后续改 SQL、看绑定关系都方便很多。7. 常见问题与排查技巧实录7.1 高频问题速查表我在用生成器的这几年里把自己踩过的坑整理成了一张速查表遇到问题直接对应查看问题现象最常见原因解决办法运行时报 ClassNotFound: com.mysql.cj.jdbc.Driver驱动依赖缺失或版本过老使用 com.mysql:mysql-connector-j 8.0.33 及以上版本生成的实体叫 TOrderInfo 而不是 OrderInfo没配 addTablePrefix在 strategyConfig 里加 .addTablePrefix(t_)生成时提示模板引擎找不到缺模板引擎依赖引入 velocity-engine-core 或对应模板引擎依赖生成的 Mapper XML 跑到了 Java 包目录下默认 XML 输出路径如此按第六章自定义输出路径实体上没有 Lombok 注解没开 enableLombok在 entityBuilder 里加 .enableLombok()启动时 Invalid bound statementmapper-locations 没覆盖到 XML 路径配置 classpath*:mapper/**/*.xml生成器重复运行时覆盖了手写代码生成器默认覆盖同名文件每次生成前先检查改动或调整配置避免覆盖7.2 生成之后必做的检查清单我自己的习惯是生成完代码之后不会马上就写业务而是先做一套固定检查先看实体类上的注解是否符合预期尤其是逻辑删除字段和自动填充字段有没有补上再看 Controller 的 RequestMapping 和类名是否符合团队规范最后启动项目确认 MyBatis-Plus 的 Mapper XML 能被正常加载没有 Invalid bound statement。这五分钟检查能帮我省掉后面一整天的联调时间。还有一个隐藏坑是数据库字段注释。生成器生成的 Swagger 注解和 JavaDoc 都是从数据库字段注释里取的你建表时写“状态 1-待支付 2-已支付”生成的注释就是这一整句如果不想要注释里带枚举说明建表时就要控制好文案。最后再分享一个小习惯如果你用生成器跑过一个旧版项目建议先把生成器入口移动到一个独立模块里管理和业务模块彻底分离。这样版本升级时不会影响线上代码重新生成也不会误伤业务文件。自动生成不是银弹但它绝对是提升日常开发幸福感的一件趁手工具用好的情况下你可以把省下来的时间花在真正需要思考的业务逻辑上。