如果你的日常也是写各种后台管理系统,那你一定感受过这种痛苦:数据库新增了一张表,接下来就是实体类、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> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.3.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency>第一个是生成器核心依赖,第二个是模板引擎。生成器默认使用 Velocity 渲染模板,如果你不加这个依赖,运行时会直接报模板引擎相关的 ClassNotFound 异常。第三个是数据库驱动,版本号根据你数据库类型调整,MySQL 8 就用这个。这里我故意把生成器依赖的 scope 写成 provided,避免它被打进生产环境的 jar 包。如果你只是临时用一次,也可以不加 scope,但跑完记得从正式依赖里删掉,否则对最终产物是一个无意义的体积拖累。
2.2 数据源连接与驱动坑点
代码生成器的第一步就是连数据库读表结构,所以连接串要提前准备好。我常用的 MySQL 连接串是这样:
String url = "jdbc:mysql://localhost:3306/demo" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8";这里三个参数你最好都带上。serverTimezone 不带,高版本驱动经常会报时区错误;useSSL 在本地开发环境没什么用,不加会输出一堆 SSL 告警;characterEncoding 不指定,生成出来的表字段注释如果是中文,容易出现乱码。实际使用中,如果你的数据库部署在远程,记得把 localhost:3306 换成实际地址,并确保当前账号有读取 information_schema 的权限,否则生成器会发现“有表但读不到任何字段”。
还有一个容易踩的坑:老版本驱动坐标是 mysql:mysql-connector-java,8.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 与 FastAutoGenerator
3.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`) ) ENGINE=InnoDB 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" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; 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 继承了 IService,OrderInfoServiceImpl 继承了 serviceImpl<OrderInfoMapper, 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?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 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 这两个字段,生成器只是把它当普通字段,并不会在插入和更新时自动赋值,所以我会补一个 MetaObjectHandler:
@Component 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 来做,这是我在老版本上验证过的方案:
List<FileOutConfig> 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 statement | mapper-locations 没覆盖到 XML 路径 | 配置 classpath*:mapper/**/*.xml |
| 生成器重复运行时覆盖了手写代码 | 生成器默认覆盖同名文件 | 每次生成前先检查改动,或调整配置避免覆盖 |
7.2 生成之后必做的检查清单
我自己的习惯是,生成完代码之后不会马上就写业务,而是先做一套固定检查:先看实体类上的注解是否符合预期,尤其是逻辑删除字段和自动填充字段有没有补上;再看 Controller 的 RequestMapping 和类名是否符合团队规范;最后启动项目,确认 MyBatis-Plus 的 Mapper XML 能被正常加载,没有 Invalid bound statement。这五分钟检查能帮我省掉后面一整天的联调时间。
还有一个隐藏坑是数据库字段注释。生成器生成的 Swagger 注解和 JavaDoc 都是从数据库字段注释里取的,你建表时写“状态 1-待支付 2-已支付”,生成的注释就是这一整句,如果不想要注释里带枚举说明,建表时就要控制好文案。
最后再分享一个小习惯:如果你用生成器跑过一个旧版项目,建议先把生成器入口移动到一个独立模块里管理,和业务模块彻底分离。这样版本升级时不会影响线上代码,重新生成也不会误伤业务文件。自动生成不是银弹,但它绝对是提升日常开发幸福感的一件趁手工具,用好的情况下,你可以把省下来的时间花在真正需要思考的业务逻辑上。