先直接说结论:MyBatisPlus的逆向工程在项目里,最常见的价值就是“根据数据库表结构,把实体类、Mapper接口、XML映射文件、Service、Controller一次性生成好”。这东西大多数人不陌生,但很多刚接触的人容易把它当成一个“生成代码的黑盒工具”,配个数据库地址点一下运行就完事了。实际上它的核心不在“生成”这一步,而在“生成前的策略配置”和“生成后的二次改造”。这篇笔记就围绕逆向工程这个主题,把我实际用下来的配置思路、踩坑记录和整理好的操作步骤完整梳理一遍。
1. 逆向工程到底解决什么问题
先聊聊为什么需要逆向工程,或者说代码生成器。做过几个正经业务项目后你会发现,CRUD代码的本质其实是“重复劳动”。一张业务表对应一个实体类、一个Mapper接口、一个XML文件、一个Service接口、一个ServiceImpl实现类,如果前后端分离,还得加一个Controller。这些代码结构高度相似,而且随着表数量增加(一张表两张表还好,二三十张表再手写,非常消耗时间),手写的效率极低。
有同学会说“IDEA有各种插件”,其实IDEA自带的数据库工具确实能生成实体类,但它的生成范围局限于单表,生成的代码也没有业务分层结构。MyBatisPlus的逆向工程之所以在团队里流传得开,是因为它生成的东西是“完整的一套分层代码”,不是只给你一个光秃秃的实体类。它能根据表结构自动推导出Java类型,能识别主键策略,能根据你的配置生成带注解的实体字段,甚至连逻辑删除、乐观锁、自动填充这些MyBatisPlus特有的功能注解都能自动加上。
1.1 它生成的具体是哪些内容
以默认推荐的一套配置来说,每次运行生成器,会得到下面这些产物:
- entity实体类:每个字段对应表的列,类型完成映射,自动加上
@TableName、@TableId、@TableField等注解,配置了Lombok的话还会自动加@Data等注解。 - mapper接口:继承
BaseMapper<T>,这意味着单表CRUD直接有了现成方法,不用自己写SQL。 - mapper XML文件:虽然单表CRUD用不上,但复杂查询的SQL不写在XML里,团队规范不允许,所以预先把文件结构和namespace生成好,后面自己往里面加业务SQL即可。
- service接口与serviceImpl实现类:继承了
IService<T>和ServiceImpl<M, T>,同样获得了一批现成的批量操作能力。 - controller类:带
@RestController和@RequestMapping注解,接口API的基础骨架都有。
这些产物看起来平淡无奇,但真实开发中很多老项目的表结构并不规范(比如表名加了统一前缀、字段命名混合了下划线和大写字母),配置好策略之后,生成出来的代码依然干净,可以直接交给团队成员继续开发。
1.2 一套配置搞定,受益于模板化
这个能力背后的核心是模板引擎。MyBatisPlus的逆向工程内置了Velocity模板(也支持Freemarker、Beetl),每类文件对应一个模板文件。默认模板生成的代码可能不完全符合你团队的习惯,但模板文件的逻辑是可以定制的。你只需要把内置模板拷贝出来改一改,然后在配置里指过去即可。这个是很多人刚开始没注意到的点,等到真正需要批量生成几十张表、又不想手工去改Controller签名时,就会发现自定义模板的价值极大。
相比从零手写代码,逆向工程的效率提升体现在“量”上。3张表跑一次生成器,N张表也是跑一次。它把所有基于表结构的代码一次性生成完毕,后续开发只需要关注真正的业务查询和接口逻辑。用生活化的类比来说,它相当于搭好了毛坯房的水电改造和墙面基础,而你只需要负责软装和空间设计,不必再去凿墙布线了。
2. 三种主流使用方式,怎么选
MyBatisPlus逆向工程有几种运行方式,分别适合不同场景。网络上资料比较多,用的时候容易混,这里直接梳理清楚。
2.1 测试类方式:最直接,适合个人开发
写一个独立的类,通常是CodeGenerator.java,放在src/test/java下面(也可以放在临时启动类里,看个人习惯),在main方法里写好配置然后运行。这种方式适合在项目搭建初期一次性生成代码,生成完毕后这个类就可以删除或者留着备用(生成的增量代码后续还能用,但盖表比较危险,所以一般用完就删,或者通过配置控制不覆盖)。
这种方式的优点是完全不用改动项目结构,不引入任何额外的Spring依赖,只要项目里有mybatis-plus、模板引擎相关依赖,写完直接run就行。缺点是每次生成代码都要打开这个类手动跑一下,没法做到“持续生成”。
比如下面的基础结构:
public class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create("jdbc:mysql://localhost:3306/demo_db", "root", "123456") .globalConfig(builder -> builder.author("某开发者").outputDir("/Users/xxx/code")) .packageConfig(builder -> builder.parent("com.example.demo")) .strategyConfig(builder -> builder.addInclude("sys_user")) .execute(); } }这个意思是连接数据库里的demo_db库,针对sys_user这一张表生成代码,输出到指定目录。看起来很简单,实际生产环境中配置会多很多,后面我会详细展开。
2.2 自定义Starter / 独立工程方式:适合团队复用
对于中大型团队,每个人都有本地的数据库源,配置可能会不一样,而且团队有自己的代码规范(比如Controller统一返回结构、实体必须继承基类、全表必须开启逻辑删除),如果每个人各自维护一份测试类,很容易出现配置漂移,导致生成出来的代码风格不统一。
这种情况下,可以考虑把生成器做成一个独立的工具模块或自定义Starter,把全部配置封装好,团队成员只需要修改数据库连接和包名两个属性即可。甚至可以做到让生成器暴露一个HTTP接口,某个表结构有变动时,调用一下接口就能重新生成增量代码(要配置好覆盖策略,不然容易把已有内容冲掉)。
这种做法的复杂度稍高,但对长期维护的好处是很明显的。我的建议是:如果团队超过3个人协作同一个项目,而且项目有明确的分层规范和代码评审流程,值得花半天把生成器统一封装一次,后面所有成员的CRUD风格会出奇地统一。
2.3 Maven插件方式:适合构建链集成
MyBatisPlus官方也提供了Maven插件(mybatis-plus-generator配合相关插件配置),可以在mvn构建时触发代码生成。这种方式适合那种希望在每次打包前自动重新生成基础代码的项目。但因为生成器会修改源码目录,如果处理不好增量覆盖问题,容易造成工作区混乱,所以实际用的人相对较少。
三者的简单对比:
| 使用方式 | 上手成本 | 适用场景 | 风格统一性 |
|---|---|---|---|
| 测试类/main方法 | 低 | 个人项目、一次性建表 | 看个人 |
| 独立工具模块 | 中 | 团队协作、多模块项目 | 高 |
| Maven插件 | 中高 | 构建链集成的工程化项目 | 高 |
就现阶段大多数项目而言,第一种方式就能解决八成问题。后面的实战部分就以这种方式展开。
3. 核心配置逐项拆解:每一项都关系到生成质量
配置是逆向工程的灵魂。不要小看这一段,很多生成结果不理想,回头看基本都是策略配置没弄对。下面拆开来看,每一步都讲清楚“为什么这么配置”。
3.1 全局配置(globalConfig):归属感和输出路径
全局配置影响的是文件“头部信息”和“存放位置”。
.globalConfig(builder -> builder .author("某开发者") .outputDir("/Users/xxx/project/src/main/java") .dateType(DateType.TIME_PACK) .commentDate("yyyy-MM-dd") .enableSwagger() )author会写到每个生成文件的@author注释里,团队内部一眼能看出这个文件是谁生成的。有些公司规范不允许个人署名,那你这里可以写团队名或直接留空。outputDir是核心,会直接影响“生成文件落在哪个模块的哪个目录”。正常来说,如果是单模块项目,直接指到src/main/java即可;多模块项目就要指到对应模块的src/main/java。但要注意,生成器不会自动创建太深的目录,最好保证路径已存在或父目录存在。dateType指定日期类型映射策略。TIME_PACK(推荐)表示使用java.time包下的LocalDateTime;ONLY_DATE是老式java.util.Date。新项目我强烈建议用TIME_PACK,不然你的实体里全是Date,配合JSON序列化框架还得额外做格式兼容。
3.2 数据源配置:逆向的前提是能连上库
这个看似简单,实际上有一堆需要注意。平常我用的MySQL数据源配置如下:
FastAutoGenerator.create( "jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai", "root", "密码")几个关键点:
characterEncoding=utf8:防止表注释、字段注释乱码。如果数据库本身就是utf8mb4,建议用characterEncoding=utf8即可兼容。useSSL=false:本地开发用不着SSL,显式关掉可以减少连接告警。serverTimezone=Asia/Shanghai:这个太重要了。很多新手第一次运行生成器报时间相关错误,十有八九就是没配时区。MySQL 8.x驱动默认要求给timezone。
如果你用的数据库是PostgreSQL、SQL Server或Oracle,驱动类名和URL前缀都要相应修改,这里不展开,基本配置逻辑是一样的,但连接串中的参数要留意各自驱动的要求差异。
3.3 包配置(packageConfig):决定你代码的“户口”
包配置直接影响生成文件的包名和目录层级。看下面的例子:
.packageConfig(builder -> builder .parent("com.example.demo") .moduleName("system") .entity("entity") .service("service") .serviceImpl("service.impl") .mapper("mapper") .xml("mapper.xml") .controller("controller") )parent是基础包名,一般是项目的顶级包。moduleName是模块名。如果你的项目是按业务模块拆包(例如com.example.demo.system表示系统模块、com.example.demo.order表示订单模块),这里就很有用。entity、mapper等子包名,默认值就是这个,一般不需要改。但注意如果你的项目里实体类不叫entity而叫domain或po,你就需要显式修改,否则生成的包名习惯和项目现有结构不一致,集成时会出现找不到类的情况。
这类配置看起来琐碎,但却是“生成代码开箱即用”的关键。因为生成的代码要和其他手写代码放在同一层,包名不一致会导致代码提交后无法统一扫描。
3.4 策略配置(strategyConfig):这一块决定实体类长什么样
策略配置是最灵活的部分。它控制生成哪些表、表名怎么转换、字段怎么映射、要不要加Lombok、要不要加逻辑删除/乐观锁注解、controller如何命名等。直接看一个比较完整的示例:
.strategyConfig(builder -> builder .addInclude("sys_user", "sys_role") .addTablePrefix("sys_") .entityBuilder() .enableLombok() .enableTableFieldAnnotation() .logicDeleteColumnName("deleted") .versionColumnName("version") .idType(IdType.ASSIGN_ID) .controllerBuilder() .enableRestStyle() .mapperBuilder() .enableMapperAnnotation() )addInclude是指定要生成哪些表。不写的话,默认扫描库里的全部表。但实际中库里往往有几十上百张表,有些关联表或临时表根本不需要生成,所以规范做法是显式列出表名,避免一次性生成一堆无用代码。
addTablePrefix("sys_")的作用是:生成实体时去掉表前缀。比如表sys_user,生成的实体类名会是User而不是SysUser。这个靠个人喜好和团队规范来取舍。如果你的库表没有统一前缀,那就不用配置。
在entityBuilder内部:
enableLombok():让生成的实体类带上@Data等Lombok注解。所有现代Java项目都可以用这个,不用再手写getter/setter。enableTableFieldAnnotation():每个字段都生成@TableField注解。这能保证当字段名与属性名映射有潜在歧义时,MyBatisPlus能精确对应上。logicDeleteColumnName("deleted"):识别表中的逻辑删除字段,并给实体字段加上@TableLogic注解。这样你调用deleteById的时候实际执行的是UPDATE而不是DELETE,这是MyBatisPlus非常核心的特性。versionColumnName("version"):如果有乐观锁字段,同样会加对应注解,配合OptimisticLockerInnerInterceptor使用。idType(IdType.ASSIGN_ID):配置主键策略为雪花算法,适合分布式环境,如果项目主键用的是数据库自增,应该改成IdType.AUTO。
在controllerBuilder内部:
enableRestStyle()会生成@RestController而不是@Controller,同时接口方法尽量用@GetMapping、@PostMapping等REST风格注解。现在前后端分离基本都是这个风格,建议开启。
在mapperBuilder内部:
enableMapperAnnotation()会在接口上加@Mapper注解。如果项目配置了@MapperScan,这里可以不开,避免重复扫描。
3.5 模板与自定义配置:真正贴近团队规范
默认模板生成的代码是标准的MyBatisPlus风格,但很多团队会定制:比如Controller要求继承一个BaseController,ServiceImpl要求带上@Service注解并额外实现一个自定义接口,实体要求继承BaseEntity并利用@FieldFill自动填充创建时间和更新时间。
遇到这些情况,简单的做法是修改生成模板。MyBatisPlus的模板默认放在依赖的jar包中,推荐做法是从源码中把需要的模板文件(如controller.java.ftl、serviceImpl.java.ftl)拷贝到项目的resources/templates目录下,按自己的规范修改,然后配置模板路径指向本地模板:
.templateConfig(builder -> builder .controller("/templates/controller.java") .service("/templates/service.java") .serviceImpl("/templates/serviceImpl.java") )注意:这里路径里如果写的是.ftl后缀就用Freemarker,如果写.vm后缀就是Velocity模板,必须与依赖模板引擎匹配。模板里的语法以对应模板语言为准。
改模板的收益是长期性的。我第一次在项目里改模板时,把Controller统一改成“请求参数用DTO、返回结构统一用R<T>”、ServiceImpl方法统一加@Transactional(rollbackFor = Exception.class)`”,改完后重新生成几十张表的代码,提交给团队评审时一次通过。这就是定制模板的价值。
4. 动手写一个最小可用的生成器实战
接下来直接展示一套能跑通的完整例子,包含建表、配置、运行、生成的完整过程。
4.1 环境准备与依赖
用Maven管理依赖(Gradle同理),项目里需要以下依赖:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>这里的版本我建议统一使用3.5.x系列,因为3.5.x之后的FastAutoGenerator是链式API风格,用起来比老的AutoGenerator简洁得多。如果项目本身还在用很老的2.x版本,大概率你需要的API是AutoGenerator而不兼容FastAutoGenerator,需要先升级依赖。
4.2 准备一张测试表
为了演示效果,这里建两张简单的表:sys_user和sys_role,其中sys_user包含逻辑删除和版本字段:
CREATE TABLE sys_user ( id BIGINT PRIMARY KEY COMMENT '主键', name VARCHAR(50) COMMENT '姓名', age INT COMMENT '年龄', email VARCHAR(100) COMMENT '邮箱', deleted TINYINT DEFAULT 0 COMMENT '逻辑删除标记', version INT DEFAULT 0 COMMENT '乐观锁版本号', create_time DATETIME COMMENT '创建时间', update_time DATETIME COMMENT '更新时间' ) COMMENT '用户表'; CREATE TABLE sys_role ( id BIGINT PRIMARY KEY COMMENT '主键', role_name VARCHAR(50) COMMENT '角色名', role_code VARCHAR(50) COMMENT '角色编码' ) COMMENT '角色表';4.3 完整生成器类
下面这个类可以放在src/test/java/com/example/demo/CodeGenerator.java,
package com.example.demo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.DateType; import com.baomidou.mybatisplus.generator.config.rules.DateType; public class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create( "jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai", "root", "123456") .globalConfig(builder -> builder .author("某开发者") .outputDir("/Users/yourname/code/demo/src/main/java") .dateType(DateType.TIME_PACK) ) .packageConfig(builder -> builder .parent("com.example.demo") .moduleName("system") ) .strategyConfig(builder -> builder .addInclude("sys_user", "sys_role") .addTablePrefix("sys_") .entityBuilder() .enableLombok() .enableTableFieldAnnotation() .logicDeleteColumnName("deleted") .versionColumnName("version") .idType(IdType.ASSIGN_ID) .controllerBuilder() .enableRestStyle() .mapperBuilder() .enableMapperAnnotation() ) .execute(); } }这个类运行后,在/Users/yourname/code/demo/src/main/java/com/example/demo/system/下会生成一系列文件,实体类会落入entity子目录,接口与实现类分别落到service、service.impl、mapper、controller子目录,XML文件落到mapper.xml子目录。
4.4 看一眼生成的实体类
以sys_user表生成的User.java为例,大致长这样:
package com.example.demo.system.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("sys_user") public class User { @TableId(type = IdType.ASSIGN_ID) private Long id; private String name; private Integer age; private String email; @TableLogic private Integer deleted; @Version private Integer version; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }这里面有几个细节值得注意:
createTime和updateTime上自动生成了@TableField(fill = ...)注解。这表示当插入或更新数据时,需要配合MetaObjectHandler实现类来批量自动填充。如果不配置对应的处理器,这些字段不会自动写值,这点需要在项目初始化时补上。@TableLogic和@Version是刚才策略配置的结果,意味着调用MyBatisPlus内置方法时,逻辑删除和乐观锁机制都会自动生效。
如果此时你发现生成的实体没有自动填充注解,那就是在策略里没有配置对应的填充字段,或者字段名和配置名不一致。遇到这种情况,先检查数据库字段名和配置名是否严格对应。
4.5 生成后需要做的二次修改
生成器不是万能的,代码生成后有几处需要手动补齐,这在任何项目里都一样。
第一,MetaObjectHandler。实体里已经标注了FieldFill.INSERT和FieldFill.INSERT_UPDATE,但还没有一个Bean真正去执行填充。手动补一个类,放到合适的位置:
@Component public class MyMetaObjectHandler 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()); } }第二,ServiceImpl里的自动填充字段和事务注解。比如UserServiceImpl生成的默认继承关系是ServiceImpl<UserMapper, User>,如果想在实现类上加@Transactional或自定义接口方法,需要手动补充。
第三,Controller里生成的只是空壳方法。只看一眼UserController.java的话,会发现它已经自动生成了getUserList、getUser、addUser、updateUser、deleteUser等接口骨架,但方法体需要按业务改造,比如加入鉴权注解、参数校验、分组校验、自定义返回结构。
这一步其实暴露了一个现实:逆向工程帮你省的是“结构性代码”的时间,业务性代码仍然要人来写。但换个角度,如果没有代码生成器,连这些空壳方法都要一个一个建,光Controller和Service的建类时间加起来就不少了,更别提XML文件里的namespace和resultMap映射。
5. 常见报错与排查实录
配置跑不通,报错信息五花八门,这里把最高频的几种情况整理一下,方便遇到问题的时候按图索骥。
5.1 连接数据库报错
常见错误之一:
Cannot load driver class: com.mysql.cj.jdbc.Driver原因基本是少了mysql-connector-java依赖,或者依赖版本与本地JDK不兼容。解决方式很简单,在pom中补上依赖并确保版本正确(MySQL 8.x对应com.mysql.cj.jdbc.Driver,老版本对应com.mysql.jdbc.Driver)。
有时还会碰到:
The server time zone value '�й���ʱ��' is unrecognized这就是典型的时区问题。在JDBC连接串中加serverTimezone=Asia/Shanghai即可。如果仍然报错,检查一下URL中是否有拼接错误或空格。
5.2 生成的表数量不对或报“表不存在”
原因通常是:
addInclude里的表名写错,表名与数据库实际不一致,注意大小写。addTablePrefix配置了前缀,但你addInclude时误用了带前缀的表名(这是正常的,但addInclude期望的是真实表名),注意别混淆。- 数据库用户权限不足,只能看到部分表(虽然少见,但确实遇到过。排查思路是换一个权限充足的账号验证)。
5.3 生成的文件找不到或目录结构不对
检查outputDir配置。比如设置为:
/Users/yourname/code/demo/src/main/java如果这个路径不存在,生成器可能不会自动创建,导致运行结束却看不到任何文件(有些版本会静默失败,非常坑)。我的做法是先把目录手动建好,或者提前在操作系统里验证路径可写。
还有一个小坑:如果把outputDir不小心设置成src/main/resources,生成的所有.java文件会躺进资源目录,编译时无法被扫描到,运行项目就会报“找不到Mapper”或“找不到实体类”。这时代码看起来生成了,实际是无效的。
5.4 生成到一半报“模板找不到”
如果你配置了自定义模板但路径写错,会看到类似:
Resource not found: /templates/controller.java.ftl模板文件默认从classpath根路径下查找。如果放在resources/templates下,配置路径应该写/templates/controller.java.ftl,注意大小写和后缀要完全一致。一个容易忽视的点,实际上在3.5.x版本中,默认模板引擎是Velocity,所以后缀为.vm;如果你依赖了Freemarker,需要按对应后缀调整。
5.5 字段类型映射不准确
常遇到的是数据库tinyint被映射成Boolean,导致生成的实体与预期不符。MyBatisPlus的默认类型转换规则中,tinyint(1)会映射成Boolean,tinyint会映射成Integer(不同数据库驱动有些差别)。如果表字段用了tinyint作为状态值,但期望映射成Integer,需要在数据库层面把字段改为smallint或者int,或者在配置里自定义类型转换器。表结构法一般不太折腾,因为一个项目的核心里字段类型应该设计合理,如果确实非改不可,使用类型转换Handler实现会需要额外代码,复杂度偏高,不太建议刚接触就上手。
5.6 覆盖已有文件导致代码丢失
生成器默认会判断文件是否已存在,很多版本对已有文件选择“跳过”。如果你改了实体结构,重新生成时发现新字段没生效,八成就是这个原因。这时需要在策略配置里开启:
.entityBuilder().enableFileOverride()但要注意:开启覆盖后,同一个文件会被整体重新写出,如果这个文件里有你手写的业务代码,会被直接覆盖掉。这可能是用逆向工程时最容易造成事故的操作。我的做法是:只有新建表或结构调整时用覆盖生成,业务代码由人工改动的文件,绝不开启覆盖。
把以上问题整理成速查表:
| 报错现象 | 常见原因 | 解决方案 |
|---|---|---|
| 无法加载驱动类 | 缺依赖或版本不匹配 | 检查pom依赖与驱动类名 |
| 时间区域识别错误 | JDBC连接串缺时区 | 加serverTimezone=Asia/Shanghai |
| 生成后找不到文件 | outputDir路径不存在 | 手动建目录或修正路径 |
| 生成了但项目不识别 | 文件生成到了resources目录 | 检查输出目录是否指到java目录 |
| 模板资源找不到 | 自定义模板路径/后缀错误 | 核对classpath路径与引擎类型 |
| 已有文件不更新 | 文件覆盖未开启 | 按需开启enableFileOverride |
| tinyint映射成Boolean | MyBatisPlus默认规则 | 改字段类型或自定义类型转换 |
这些坑我基本都踩过一遍,尤其是覆盖问题,曾经出现过一次重新生成把Service实现类里手写的业务方法直接冲掉的情况,从那以后我对覆盖严格控制,宁可多花一分钟检查文件是否存在,也不想再经历一次代码丢失。
6. 命名策略与团队规范化的一些实战心得
最后单独聊聊命名策略和规范化,因为这是很多团队用了生成器后依然觉得“代码很乱”的核心原因,不是生成器的问题,而是约定没做好。
6.1 表名与字段命名要统一
项目里最怕两种风格混用:有的表叫user_info,有的表叫UserInfo,有的字段叫userName,有的字段叫user_name。生成器对字段名有默认的下划线转驼峰能力,但表名不一致时,生成的实体名会很混乱。建议项目立项阶段就定下表名规范:全小写加下划线,多词用下划线分隔,统一前缀(如sys_、biz_)。这样addTablePrefix才能稳定发挥作用,生成的实体才能保持清晰一致。
6.2 不要试图让生成器生成所有代码
我在实践中的体会是:逆向工程最适合生成“基础CRUD层”和“基础结构层”,不适合生成复杂业务逻辑。比如一个查询需要关联5张表并且带动态条件,这种SQL即使让生成器生成也意义不大,它无法理解你的业务过滤条件。相反,selectById、selectList、selectPage、insert这类代码生成得再好,也只是一个起点。
具体到Controller层,生成的代码只是空壳,我觉得理想的做法是:实体、Mapper、Service生成后,Controller层不直接使用而是把业务方法补齐到Service实现类里,Controller只做参数接收和返回结构包装。这套流程更利于后期维护。
6.3 模板定制的正确姿势
很多人在第一次接触自定义模板时,恨不得把模板改得非常复杂,功能加了一堆。踩过几次坑之后我的经验是:模板只需要控制“统一风格”的几件事即可,例如类的注释格式、类名后缀、继承基类、是否启用Lombok、接口是否需要@Mapper注解。不要把业务逻辑写进模板,模板里的逻辑越多,后面改起来越痛苦。
如果要用自定义模板,建议先在本地用一个简单表跑通改模板流程,确认生成结果没问题后再全量应用。因为模板语法调试比较费时间,而且在IntelliJ IDEA里直接改FTL文件,语法提示比较弱,耐心一点点排查。
6.4 多模块项目的生成器配置
多模块项目要注意包路径与依赖方向。比如一个典型的分层多模块项目:common(基础类)、dal(数据访问)、service(业务)、web(接口)。生成实体、Mapper和XML时要指向dal模块,生成Controller和DTO时要指向web模块。实际使用中可以在同一个生成器类里调用两次execute(),分别生成不同模块的代码(配置的包名和输出目录不同即可)。但如果项目模块之间依赖复杂,建议分成两个独立的生成器类,避免一份配置杂糅,导致生成目标不清晰。
6.5 生成器与增量开发结合
有些团队的数据库设计是通过SQL脚本维护的,我比较推荐的做法是:每次表结构调整后,先更新SQL脚本,再运行生成器增量生成。也就是说,每次运行生成器时不会删除已有的代码,只是新增缺失的字段和文件。这种方式的关键是“区分哪些文件可以被覆盖、哪些文件不能被覆盖”。具体到操作上,我会把实体类、Mapper接口、XML这些“结构型文件”允许覆盖(但要保证没有手写内容),而Service实现类和Controller标记为“手写保护”,永远不走覆盖。这样既能保持代码更新,又不会冲掉业务方法。
7. 一点个人体会
用了这么多年代码生成器,我越来越觉得它真正解决的其实不是“写代码”的问题,而是“减少重复决策”的问题。每张表都去手动建一套CRUD,看起来每张表花不了多少时间,但几十张表下来,消耗的精力和心智会很多,而且这些时间本来应该花在真正的业务逻辑上。
另外,生成器配合规范的收益是成倍的。如果团队里大家手工写实体时有的人加@Data、有的人写getter/setter,有的人Controller用@RestController、有的人用@Controller,代码风格会很杂乱。而有了统一的逆向工程配置,等于把团队里的“基础代码风格”自动统一了,新成员接手项目时也能快速理解代码结构。
如果你想用好逆向工程,我的建议是不要只停留在“能跑通”的层面,花点时间把策略配置里的每个选项都过一遍,哪怕只是跑一个测试表,看看开启和关闭某个选项时生成的代码有什么区别。理解了每个配置项的实际效果之后,你就不再是“照着别人博客抄配置”,而是真正理解这套工具的设计逻辑了。
最后还是那句老话:工具是死的,用法是活的。真正厉害的项目,不是靠生成器一键生成的,而是靠团队把生成规则、命名规范、覆盖策略、二次开发流程都定得明明白白,再把生成器嵌进日常开发节奏里,让它成为团队效率的一部分。