
先交代一下背景这个系列笔记默认你已经把 MyBatis-Plus 的 CRUD、条件构造器、分页插件、逻辑删除这些基础功能都过了一遍能正常写业务代码了。这次聊的是“逆向工程”也就是用工具根据数据库表结构直接生成实体类、Mapper 接口、Service、ServiceImpl 和 Controller 这一整套代码。我尽量把配置思路和踩坑点写全照着实际操作的时候能省掉大半的查文档时间。1. 为什么还需要逆向工程可能有人会觉得现在都流行写 SQL 映射自己建实体类或者用 JPA 那种自动建表的方式逆向生成代码是不是过时了我个人的观点是只要是“数据库表先存在、且表结构相对稳定”的项目逆向工程依然是效率最高的起步方式。尤其是接手老系统、从别处同步表结构、或者数据库设计文档特别详细但代码还是一片空白的场景生成器一次性产出的这堆基础代码能帮你省掉一下午的机械劳动。1.1 手工编写实体类的三个痛点第一字段映射容易出错。数据库字段一般是下划线命名Java 属性是驼峰命名比如user_login_name对应userLoginName手工写多了总有对不齐的时候轻则查询结果少字段重则运行时 SQL 报错。第二CRUD 代码高度重复。每个表的 Mapper 接口、XML 文件、Service 接口、ServiceImpl 实现类结构性都是一模一样的。这种代码写三五个还能忍写五十个就会开始犯困。第三类型映射规则琐碎。数据库的datetime、timestamp、decimal、tinyint该对应 Java 里的什么类型每个团队都有自己约定手动敲的时候很容易出现同一个字段在不同类里类型不一致的情况。1.2 什么项目场景最适合用生成器不是说所有项目都适合但下面这些场景几乎是无脑收益后端接口以单表操作为主表之间关联不太复杂。数据库模型由 DBA 或设计文档先行定义代码要跟着表结构走。项目处于快速迭代期每周可能有新表加入生成一次就能跑。团队内已经统一了代码规范生成器的默认输出正好符合规范。如果项目是领域驱动设计导向、建表本身也是跟着代码走的那逆向工程的价值会弱化这种情况也别硬上工具合适不合适自己心里要有数。2. 生成器的依赖与版本选择MyBatis-Plus 官方提供的代码生成器叫做 Mybatis Generator 的增强版直接引入mybatis-plus-generator这个依赖就行。不过这块有个比较坑的版本差异需要先搞清楚。2.1 新版 API 和旧版 API 的区别在 3.5.3 之前的版本生成器主类写法是用AutoGenerator配合GlobalConfig、DataSourceConfig、PackageConfig、StrategyConfig这些配置类。3.5.3 版本之后官方推荐用FastAutoGenerator写法更简洁链式调用一目了然。我这两套都写过直接说结论新项目用FastAutoGenerator没有理由再去翻旧 API。FastAutoGenerator 把配置项收敛得更紧凑语义清晰而且官方文档和社区讨论目前都集中在这个新 API 上遇到问题也更容易搜到答案。2.2 依赖配置必备清单一个最简配置只需要两个依赖前提是你的项目本身已经引入了 MyBatis-Plus 的核心依赖。核心依赖就是mybatis-plus-boot-starter如果你工程里还没加按照官方 start 页面的标准配置引入即可。生成器本身要引入dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-generator/artifactId version3.5.7/version /dependency然后是模板引擎。生成器默认用的是 Freemarker但代码里你会看到.templateEngine(new FreemarkerTemplateEngine())这样的写法所以必须引入对应的引擎依赖dependency groupIdorg.freemarker/groupId artifactIdfreemarker/artifactId version2.3.32/version /dependency提示如果你喜欢 Velocity 或者 Thymeleaf也可以换成对应的引擎依赖个人建议直接用 Freemarker因为生成器默认模板就是按 Freemarker 写的换了引擎还需要额外处理模板文件没必要。另外还需要 JDBC 驱动。如果你连的是 MySQL 8.x记得用com.mysql.cj.jdbc.Driver这个驱动类同时 URL 里带上时区参数不然很容易报时区相关的异常。dependency groupIdcom.mysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scoperuntime/scope /dependency3. 生成前必须搞懂的配置参数代码生成器这东西配置好了是效率神器配错了就是批量生产垃圾代码的流水线。以下这几个配置项我建议你在第一次跑生成之前就逐行理清楚。3.1 全局配置输出目录与作者名全局配置里最常见的两个参数是输出目录和作者名。.globalConfig(builder - builder .outputDir(/Users/yourname/projects/demo/src/main/java) .author(你的名字) .dateType(DateType.TIME_PACK) .commentDate(yyyy-MM-dd) )这里的outputDir是最终代码输出的根目录。很多新手会在这个地方犯迷糊写成src/main/java或者干脆填成绝对路径。我的建议是生成器只负责产出代码生成完之后立即去目录里核对然后把生成的代码复制到正式工程里不要让生成器直接往你正在开发的工程目录里乱写一旦覆盖错了文件哭都来不及。author填谁的名字生成的每个文件头注释里就会跟着谁的名字这个自己按团队习惯填就行。dateType比较关键。默认值生成的时间类型是 Java 8 之前的java.util.Date现在大多数项目的数据库时间字段都应该用java.time.LocalDateTime所以这里务必要显式设成TIME_PACK。3.2 包名与路径配置决定代码落在哪包配置直接决定了生成出来的类在哪个包下面。.packageConfig(builder - builder .parent(com.example.demo) .moduleName(system) .entity(entity) .service(service) .serviceImpl(service.impl) .mapper(mapper) .controller(controller) )这样生成出来的完整类名就是com.example.demo.system.entity.User这样的格式。需要注意两点一是moduleName这个参数。如果你的表有前缀比如sys_user、sys_role而你希望生成的代码单独放在一个system模块下这个参数很好用。但如果你所有表都在一个业务包下那就把moduleName设为空字符串不要画蛇添足。二是mapper接口生成后默认的 XML 映射文件路径是在resources/mapper目录下这个路径不在包配置里需要在后续的模板配置或者复制文件时手动处理。很多项目就是在这里对不上导致启动时Invalid bound statement报错。3.3 策略配置连表名字的转化规则都要管策略配置是整份配置里最有技术含量的部分。核心几个参数.strategyConfig(builder - builder .addInclude(sys_user, sys_role) .addTablePrefix(sys_) .entityBuilder() .enableLombok() .logicDeleteColumnName(deleted) .versionColumnName(version) .controllerBuilder() .enableRestStyle() .mapperBuilder() .enableBaseColumnList() .enableBaseResultMap() )addInclude是白名单只生成你指定的表。千万别不加这个参数直接跑默认行为是把数据库里所有表全生成一遍如果你的数据库里有几十张表那画面太美我不敢看。addTablePrefix负责去掉表名前缀。比如sys_user去掉了sys_之后实体类名就是User而不是SysUser。这个规则要配合addInclude一起用先用白名单圈定范围再告诉生成器哪些前缀该去掉。entityBuilder().enableLombok()表示生成的实体类使用 Lombok 的Data注解而不是手写 getter/setter。现在新项目基本没有手写 getter/setter 的了里面有个enableTableFieldAnnotation()参数建议打开这样每个字段上都会加上TableField注解后续改字段名映射时能少很多排查工作。logicDeleteColumnName和versionColumnName这两个参数对应的是 MyBatis-Plus 的逻辑删除和乐观锁功能。如果你项目里已经用了逻辑删除deleted字段、乐观锁version字段在生成器这里配置好了产出的实体类会自动带上TableLogic和Version注解省去手工加注解的步骤。controllerBuilder().enableRestStyle()生成的是RestController风格的接口而不是传统Controller加ResponseBody的方式这个看团队习惯现在新项目基本都用的前者。mapperBuilder().enableBaseResultMap()和enableBaseColumnList()会生成基础的ResultMap和通用字段列表。如果你要用自定义 SQL 并且希望复用到字段映射这两个开启可以少写很多 XML 里重复的result标签。3.4 自定义模板让代码带上团队的味道FastAutoGenerator 支持自定义模板文件你可以把官方默认的模板文件拷贝一份改完之后再让生成器指向你改过的模板。.templateConfig(builder - builder .entity(/templates/entity.java) .service(/templates/service.java) .serviceImpl(/templates/serviceImpl.java) .mapper(/templates/mapper.java) .controller(/templates/controller.java) )模板文件默认位置在依赖包com.baomidou.mybatisplus.generator.engine的同级目录下你可以从本地 Maven 仓库里把对应的.ftl文件拷出来看一遍基本就能照着改。这里只想表达一个观点不要过度自定义模板。生成器的意义在于快速产出统一规范的代码如果你在模板里塞了一堆团队特有的业务逻辑那生成器产出的代码反而成了需要反复维护的重灾区。最多加个类注释、统一 import 顺序、引入团队自己的 BaseController其他的都保持默认即可。4. 完整实操从数据库到代码一次跑通说了一堆配置原理不实际跑一遍总觉得隔层纱。下面我从建表、配置、执行、验证四个环节完整演示一次。4.1 准备一张示例表假设你现在要做用户和角色管理有两张表。我在本地建一个测试库来演示CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, user_login_name varchar(50) NOT NULL COMMENT 登录名, user_password varchar(100) NOT NULL COMMENT 密码, user_nickname varchar(50) DEFAULT NULL COMMENT 昵称, email varchar(100) DEFAULT NULL COMMENT 邮箱, phone varchar(20) DEFAULT NULL COMMENT 手机号, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1启用0禁用, deleted tinyint NOT NULL DEFAULT 0 COMMENT 逻辑删除标记, version int NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time datetime NOT NULL COMMENT 创建时间, update_time datetime NOT NULL COMMENT 更新时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;这张表设计上特意包含了逻辑删除字段deleted、乐观锁字段version、下划线命名字段覆盖了大多数项目里会遇到的情况。角色表结构类似就不完整贴出来了。4.2 编写生成器主类为了让代码清晰一点我单独建一个CodeGenerator.java作为启动类package com.example.demo.generator; import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.OutputFile; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; import java.util.Collections; public class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create( jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai, root, your_password ) .globalConfig(builder - builder .author(dev_team) .outputDir(/Users/yourname/projects/demo/src/main/java) .dateType(com.baomidou.mybatisplus.generator.config.DateType.TIME_PACK) .commentDate(yyyy-MM-dd) ) .packageConfig(builder - builder .parent(com.example.demo) .moduleName(system) .entity(entity) .service(service) .serviceImpl(service.impl) .mapper(mapper) .controller(controller) .pathInfo(Collections.singletonMap(OutputFile.xml, /Users/yourname/projects/demo/src/main/resources/mapper)) ) .strategyConfig(builder - builder .addInclude(sys_user, sys_role) .addTablePrefix(sys_) .entityBuilder() .enableLombok() .enableTableFieldAnnotation() .logicDeleteColumnName(deleted) .versionColumnName(version) .controllerBuilder() .enableRestStyle() .mapperBuilder() .enableBaseResultMap() .enableBaseColumnList() ) .templateEngine(new FreemarkerTemplateEngine()) .execute(); } }这段代码有几个细节值得展开说明。pathInfo里配置了 XML 文件的输出路径。刚才提到包配置里没有 XML 的路径就是通过这个OutputFile.xml键来指定的这里填成resources/mapper目录生成之后直接就在 classpath 里了启动时能自动扫到。addInclude(sys_user, sys_role)明确告诉生成器只处理这两张表。我第一次用生成器时就是忘了加这个参数结果把一个库十几张业务表全生成了文件多到 IDE 都卡了一会儿那之后我就养成了先写addInclude再写其他配置的习惯。4.3 执行生成与产物验证直接运行main方法控制台会输出类似下面的日志[main] INFO com.baomidou.mybatisplus.generator.AutoGenerator - 配置信息 [main] INFO com.baomidou.mybatisplus.generator.AutoGenerator - 准备生成文件... [main] INFO com.baomidou.mybatisplus.generator.AutoGenerator - Entity 输出: /Users/.../entity/SysUser.java [main] INFO com.baomidou.mybatisplus.generator.AutoGenerator - Service 输出: /Users/.../service/SysUserService.java [main] INFO com.baomidou.mybatisplus.generator.AutoGenerator - ServiceImpl 输出: /Users/.../service/impl/SysUserServiceImpl.java [main] INFO com.baomidou.mybatisplus.generator.AutoGenerator - Mapper 输出: /Users/.../mapper/SysUserMapper.java [main] INFO com.baomidou.mybatisplus.generator.AutoGenerator - Controller 输出: /Users/.../controller/SysUserController.java [main] INFO com.baomidou.mybatisplus.generator.AutoGenerator - 生成完成注意日志里显示的是SysUser.java而不是SysUser类名。因为表名是sys_user实体类名按驼峰规则转化后是SysUser这个命名是符合常规的。生成的SysUser.java大概长这样package com.example.demo.system.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.Version; import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; Data TableName(sys_user) public class SysUser implements Serializable { TableId(value id, type IdType.AUTO) private Long id; private String userLoginName; private String userPassword; private String userNickname; private String email; private String phone; private Integer status; TableLogic private Integer deleted; Version private Integer version; private LocalDateTime createTime; private LocalDateTime updateTime; }这里可以看到逻辑删除字段和乐观锁字段自动带上了对应注解LocalDateTime类型也按全局配置正确映射了。这些在手工写的时候特别容易漏生成器一次性全处理好了。生成的 Mapper 接口也值得一提Mapper public interface SysUserMapper extends BaseMapperSysUser { }接口上是带Mapper注解的所以即使你的项目没有配置MapperScan只要 Spring Boot 启动类能扫到当前包这个 Mapper 也能被正常装配。最后把生成的代码整体复制到正式工程的对应目录启动一次应用跑一下查询接口验证 Mapper 映射正常、字段值能取到整个流程就闭环了。5. 实战排坑我踩过的那些坑代码生成器本身不复杂但实际用起来每个项目都可能有自己的特殊情况。下面这几个坑是我自己和身边同事在项目里真实遇到过的列出来当个快速排查手册用。5.1 表前缀处理失当有个同事的项目科目标签叫t_xxxx他在配置里写了addTablePrefix(t_)本来想着生成出来的实体叫Xxxx结果输出TXXXX因为他的表名是t_xxxx而t_并不是标准前缀之一。常见的前缀规则是sys_、t_、tb_这三种都试一下生成后检查实体类名是否符合预期再批量复制。还有种情况是多个前缀混合比如一部分表是sys_user另一部分是cfg_config这时候addTablePrefix(sys_, cfg_)就行了生成器会逐个匹配。5.2 主键 ID 策略和数据库对不上默认的IdType可能是ASSIGN_ID雪花算法但如果你表的主键是数据库自增那生成出来的实体在插入时不会带主键值数据库自增生效。反过来表用了雪花 ID 而配置成自增批量插入时会出现主键冲突。解决方式就是数据库主键自增TableId(type IdType.AUTO)应用层生成雪花 IDTableId(type IdType.ASSIGN_ID)这个在策略配置里可以统一设置.entityBuilder() .idType(IdType.AUTO)顺带一提如果你用了分库分表组件主键策略往往不是自增也不是雪花而是由中间件统一分配那就要在模板配置里特殊处理了这类情况较少就不展开了。5.3 覆盖源码的风险生成器默认会检查文件是否已存在如果存在会跳过还是覆盖不同版本的默认行为不完全相同但为了安全起见我建议你每次生成前把当次要生成的代码目录整个备份一下或者干脆用一个独立的临时目录输出。第一次生成时我用默认覆盖策略冲掉了一个手改过大量逻辑的 ServiceImpl那感觉真是窒息。如果你确定要重新生成某个表的全部代码可以先手工删掉旧文件再重新生成宁慢勿错。5.4 时间类型的映射踩雷刚提到dateType(TIME_PACK)可以把datetime映射成LocalDateTime但有个容易忽略的细节并不是所有数据库时间字段都该这么做。比如某些统计表的日期字段是date类型你想要的是LocalDate但TIME_PACK会把date也映射成LocalDateTime这在语义上是不合适的。解决方案是在实体类生成后手动调整这几处类型或者把数据库字段类型改掉如果还在设计阶段且允许改表的话。这类特殊映射目前生成器没有提供细粒度的控制参数只能靠生成后手工修正数量不多也还好。5.5 多模块工程的路径问题如果你的项目是 Maven 多模块结构比如常见的demo-common、demo-system、demo-api三个模块生成器的outputDir必须单独指向对应模块的src/main/java否则代码会全部落在启动模块里。还有就是OutputFile.xml的路径多模块下 XML 往往会放在demo-system/src/main/resources/mapper/里这个在pathInfo里按实际路径填即可。最容易出错的是parent和moduleName的组合在多模块结构下parent应该填模块内已有的包前缀比如com.example.system然后moduleName留空否则会生成com.example.demo.system.system.xxx这种嵌套地狱。6. 从生成代码到工程落地的必要调整生成器产出的代码能直接跑但离“能上线”还有几步路。这里讲几个我每次生成之后都会做一遍的统一调整。6.1 启动类扫描路径检查如果你项目启动类上只写了SpringBootApplication默认扫描的是启动类所在包及其子包。生成器默认会把包名按你配置的parent写进去所以只要parent和启动类的包一致一般都能扫到。如果你项目里手动配置过MapperScan(com.example.demo.**.mapper)那要注意通配符写法是否正确不然 Mapper 注入会失效。6.2 通用 BaseEntity 的取舍很多团队的实体类都有公共字段比如createTime、updateTime、deleted、version这些字段每张表都有。生成器默认会把所有字段全量写进每个实体类这会导致重复代码。两种思路一是接受重复实体类保持完整适合表之间公共字段不完全一致的场景。二是用 MyBatis-Plus 的BaseEntity基类把公共字段抽出来实体继承它。如果你选择第二种可以在生成器配置里指定父类.entityBuilder() .superClass(com.example.demo.common.BaseEntity)同时还需要在addIgnoreColumns里把公共字段排除掉避免父类和子类字段重复。6.3 Service 层要不要生成接口标准的三层架构里 Service 接口加 ServiceImpl 是标配但有些轻量项目其实可以只要 ServiceImpl不要接口。生成器默认两个都生成如果你们团队约定不需要接口可以在自定义模板里去掉 Service 接口的输出或者生成之后统一删除。我个人在内部项目里倾向于要接口方便写单元测试时做 mock。7. 提升生成效率的扩展玩法基础用法的上限大概就到这了下面分享几个让生成器效率倍增的思路。7.1 结合 Swagger 注解生成器默认模板生成的实体类上是没有ApiModelProperty这类注解的但如果你的模板自定义了就可以自动把数据库字段的COMMENT内容带进去。我是在模板文件里加了一行ApiModelProperty(value ${field.comment})注意要先把表字段注释写好很多表建表时COMMENT是空的生成出来的注解自然也是空字符串这种情况可以先在数据库补好注释再重新生成注释质量直接影响接口文档的观感。7.2 批量生成时用表名筛选数据库表特别多的时候建议在配置里加一个忽略规则而不是把几十个表名一个一个写进addInclude。官方提供了addIgnore配置支持正则匹配.strategyConfig(builder - builder .addInclude(^sys_.*) )这样所有sys_开头的表都会进入生成范围。这里要提醒这种通配方式用起来方便但生成之前最好先检查一下你是否真的想把所有sys_表都生成代码有些可能是临时表或者定时任务同步表生成了反而增加代码量。7.3 生成自定义 SQL 片段有些复杂的报表 SQL 并不适合写在 Mapper XML 的通用 CRUD 里。生成器允许在模板中预留块或者你在生成后用自定义脚本把额外的select标签追加进 Mapper XML。这个操作没有统一的官方做法我在实际项目里就是写了一段简单的 Java 脚本按表名找对应 XML 文件在尾部插入定制的查询语句配合Select注解使用效果也还可以。这块完全看个人需求不做硬性结论。8. 最后聊一下个人实践中的体会用生成器这么多年我最大的体会就是它解决的是“体力活”而不是“脑力活”。它把重复的、容易手滑的、检查成本高的代码生成工作自动化了但业务逻辑的复杂度、表设计的合理性、接口返回结构的规划还是得靠人来做。所以别指望生成器能直接生成一个能上生产的 Controller那里面往往是空壳的方法和注释业务代码还是得自己填。实际用的时候我习惯把生成器单独放在一个generator子模块里不参与主项目的打包这样生成代码的依赖不会污染线上依赖。生成完代码之后一定要肉眼过一眼关键配置类特别是TableName和TableField的映射结果一旦发现数据库和类字段对不上趁早改别等到运行时报错再回头查。如果你所在的项目也正在纠结要不要用生成器或者已经被几十张表的实体类折磨得有点不耐烦不妨照着上面的配置跑一次把生成结果和手工写的对比一下再决定是不是要继续手工敲键盘。