写Java后端的人,大概都有被CRUD支配过的经历。实体类加一个字段,Controller、Service、Mapper、XML全要跟着动;新表建好,光把那套标准文件补齐就要小半天。我第一次用MyBatis-Plus代码生成器时,只当它是个快速生成类的小工具,后来在项目里用熟了才发现,它的真正价值不在于省几行代码,而是把团队几十个模块的CRUD风格拉齐,让每个人生成的类长得一模一样。最近MyBatis-Plus把代码生成器重写成了FastAutoGenerator,老教程里的AutoGenerator写法已经过时,网上很多资料还停在旧版。这篇文章就结合我在新项目里的实操,把新版代码生成器的配置、批量生成过程和落地时的几个关键经验一次讲完。
1. 新版代码生成器为什么值得升级
1.1 从AutoGenerator到FastAutoGenerator:API变在哪
旧版代码生成器的痛点,用过的人应该都有体会。整个生成流程要分别创建DataSourceConfig、GlobalConfig、PackageConfig、StrategyConfig,然后通过setter方法逐个塞进AutoGenerator里。配置项又多又分散,第一次用的时候不看文档根本记不住要配哪些。而且旧版换到新版,API几乎推倒重来,网上的老教程抄过来直接把入门劝退。
新版FastAutoGenerator是MyBatis-Plus 3.5.1之后引入的入口类,核心变化就是链式调用。数据源直接作为create方法的入参,全局配置、包配置、策略配置各自通过一个builder闭包完成,最后execute()执行。代码结构从“声明多个对象再组装”变成了“一条流水线走完”,配置集中、层次清楚,从阅读到修改都友好得多。
// 旧版写法 AutoGenerator generator = new AutoGenerator(); generator.setDataSource(dataSource); generator.setGlobalConfig(new GlobalConfig().setOutputDir("D:/code")); generator.setPackageInfo(new PackageConfig().setParent("com.demo")); generator.setStrategy(new StrategyConfig().setInclude("user")); generator.execute(); // 新版写法 FastAutoGenerator.create("jdbc:mysql://localhost:3306/demo", "root", "123456") .globalConfig(builder -> builder.outputDir("D:/code")) .packageConfig(builder -> builder.parent("com.demo")) .strategyConfig(builder -> builder.addInclude("user")) .execute();旧版和新版的对比如下。
| 对比项 | 旧版 AutoGenerator | 新版 FastAutoGenerator |
|---|---|---|
| 入口方式 | 创建多个配置对象再set进总类 | create方法一条链式流水线 |
| 数据源配置 | 单独new DataSourceConfig | create(url, username, password)直接传入 |
| 配置结构 | 分散在多个setter对象中 | globalConfig/packageConfig/strategyConfig闭包内集中配置 |
| 批量表生成 | 用StrategyConfig.setInclude传数组 | strategyConfig里addInclude一次传多个表名 |
| 可读性 | 配置项之间关系不直观 | 生成逻辑从上到下逐个可见 |
新版并没有增加什么革命性的生成能力,它改的是使用体验。把散落的状态集中起来,把需要记忆的配置位置变成IDE的代码提示,这套设计思路其实值得所有工具类项目借鉴。
1.2 它真正解决的问题:不只是省事
代码生成器的本质,是把标准单表CRUD这个过程工业化。对于一张业务表,实体、Mapper、Service、ServiceImpl、Controller、XML,八成内容都是强模式:表名映射、主键策略、字段注解、分页查询、保存、更新、删除。这些代码人工写不难,但很容易反复,更危险的是容易漏。漏加@TableName、漏改Mapper XML里的resultMap、Controller分页参数写错,都是我在实际项目中踩过的坑。生成器把这些重复劳动全部机械化,出错概率降到最低。
容易被忽略的是,生成器还能当团队的代码规范执行器。之前带过一个项目组,五个同事写Controller风格各不相同:有人用@RestController,有人用@Controller,有人返回实体类,有人返回Map。后来把生成器主类统一配好,默认生成REST风格接口和标准分层代码,新同学来了照着生成结果写,风格就不会跑偏。这比贴一页规范文档管用得多。
当然也要说清楚边界。多表关联查询、复杂业务事务、审批流、状态机,这些生成器统统搞不定。它擅长的是单表标准CRUD骨架,生成完之后的业务方法还得自己补。把它当脚手架,别当万能引擎,心态就对了。
2. 生成之前要想清楚的三个配置
2.1 数据源配置是生成器的唯一入口
代码生成器的原理,是连接数据库后读取表结构信息,再根据表结构生成Java代码。所以数据源配置是硬前提,必须保证能连上目标库、能读取到目标表。
我常用的MySQL连接串是这样的:
jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true几个关键参数要说一下。characterEncoding=utf8是为了避免生成注释时出现中文乱码;serverTimezone必须指定,否则高版本MySQL驱动会报时区错误;useSSL=false和allowPublicKeyRetrieval=true是MySQL 8.x的常见配置,不加allowPublicKeyRetrieval容易遇到“Public Key Retrieval is not allowed”的报错。
注意:生成器只读取表结构,不会修改数据库数据,所以不用担心误操作。直接用项目所用的账号即可,但建议有SELECT权限的账号就够用,最小化权限是实操中的好习惯。
驱动依赖别漏。MySQL 8及以上版本要用com.mysql.cj.jdbc.Driver,项目的JDBC连接下不需要显式设置driverClassName,FastAutoGenerator能根据URL自动识别,但依赖必须引入。
2.2 全局配置和包配置决定代码落在哪
初次使用的人最容易栽在输出目录上。FastAutoGenerator的globalConfig里如果不显式设置outputDir,不同版本对默认输出目录的处理并不一致,有些会落到系统临时目录下,跑完之后满世界找不到生成的文件。所以我建议一律使用绝对路径,并且是项目内的明确目录。
.globalConfig(builder -> builder .outputDir("D:/workspace/mall/mall-generator/src/main/java") .author("your-name") .enableSwagger() .enableFileOverride())outputDir表示生成Java代码的根目录,注意是src/main/java,不是项目根目录。如果生成在项目根目录下,文件放错位置,Maven编译根本不认识。packageConfig负责生成文件的包路径结构:
.packageConfig(builder -> builder .parent("com.example.mall") .moduleName("system") .entity("entity") .mapper("mapper") .service("service") .serviceImpl("service.impl") .controller("controller") .pathInfo(Collections.singletonMap(OutputFile.xml, "D:/workspace/mall/mall-generator/src/main/resources/mapper")))parent加上moduleName是基础包名。上面这段配置生成出来的实体路径就是com.example.mall.system.entity,Service实现路径是com.example.mall.system.service.impl。这一层命名一定要想好再定,因为生成之后再改包名很痛苦。Mapper的XML和Java代码不在同一个目录,通过pathInfo单独指定到src/main/resources/mapper下。如果这里忘了配,XML文件就可能生成到java目录里,启动时扫不到,后面排查非常麻烦。
2.3 策略配置决定哪些表生成、以什么形态生成
策略配置是生成器最灵活的地方,它决定三件事:生成哪些表、表名怎么处理、字段注解如何映射。
最核心的addInclude用来指定要生成的表名。想批量生成,直接一次性传入多个表名:
.strategyConfig(builder -> builder .addInclude("t_user", "t_order", "t_goods") .addTablePrefix("t_"))addTablePrefix表示把表名前缀去掉。表名t_user生成实体类时只保留User,避免实体类名变成别扭的TUser。前缀处理还有其他选项,比如addTableSuffix、字段前缀,但按我的经验最常用的还是表前缀。
实体的生成形态也是在策略里控制的。想让实体类使用Lombok注解而不是满屏getter/setter,可以这样写:
.strategyConfig(builder -> builder .addInclude("t_user") .addTablePrefix("t_") .entityBuilder() .enableLombok() .enableTableFieldAnnotation() .logicDeleteColumnName("deleted") .versionColumnName("version") .controllerBuilder() .enableRestStyle())这段配置里几个关键项的效果分别是:enableLombok让实体生成@Data注解,不再生成冗长的getter/setter;enableTableFieldAnnotation让实体字段标注@TableField,表字段和Java属性严格对应;logicDeleteColumnName和versionColumnName指定逻辑删除字段与乐观锁字段,方便后续业务直接使用这些能力;enableRestStyle让Controller生成@RestController而不是@Controller,同时方法返回结果也更现代化。定期生成的时候,把这些开关一次性定好,后面所有表都按同一套规则生成。
3. 实战:一次生成用户、订单、商品三个模块
3.1 依赖怎么加
我这次用的是Spring Boot项目,MyBatis-Plus版本是3.5.x。pom里需要引入mybatis-plus-boot-starter、mybatis-plus-generator以及模板引擎依赖。
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.32</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>这里有两个注意点。第一,mybatis-plus-generator的版本尽量和mybatis-plus-boot-starter保持一致,否则可能出现不兼容的API调用。第二,FastAutoGenerator默认使用的模板引擎是Velocity,如果你像我一样用Freemarker,就必须显式引入freemarker依赖,并在生成器代码里指定templateEngine(new FreemarkerTemplateEngine())。依赖缺失时运行会直接报类找不到,这个坑很常见。
3.2 生成器主类完整写法
生成器做成了一个独立的main方法,放在项目单独的tools包下,和业务代码隔离。完整代码如下:
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 MyBatisPlusGenerator { public static void main(String[] args) { String url = "jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true"; String username = "root"; String password = "123456"; FastAutoGenerator.create(url, username, password) .globalConfig(builder -> builder .outputDir("D:/workspace/mall/mall-generator/src/main/java") .author("your-name") .enableSwagger() .enableFileOverride()) .packageConfig(builder -> builder .parent("com.example.mall") .moduleName("system") .entity("entity") .mapper("mapper") .service("service") .serviceImpl("service.impl") .controller("controller") .pathInfo(Collections.singletonMap(OutputFile.xml, "D:/workspace/mall/mall-generator/src/main/resources/mapper"))) .strategyConfig(builder -> builder .addInclude("t_user", "t_order", "t_goods") .addTablePrefix("t_")) .templateEngine(new FreemarkerTemplateEngine()) .execute(); } }enableFileOverride这个配置值得单独拎出来说。FastAutoGenerator默认不覆盖已经存在的文件,也就是说同一个表如果生成过一次,再次执行时不会重新生成,改了表结构也看不到变化。如果希望每次生成都强制覆盖,必须显式开启这个开关。我第一次用新版生成器时没开它,改完表字段再跑生成,发现实体类还是老样子,排查了半天才发现是这个默认行为。
3.3 跑起来之后能看到什么
执行main方法后,控制台会打印SQL执行日志和生成过程。只要数据源能连通、表名写对,几秒之内就能生成完毕。生成后的目录结构是这样:
mall-generator/src/main/java/com/example/mall/system/ ├── controller │ ├── TOrderController.java │ ├── TGoodsController.java │ └── TUserController.java ├── entity │ ├── TOrder.java │ ├── TGoods.java │ └── TUser.java ├── mapper │ ├── TOrderMapper.java │ ├── TGoodsMapper.java │ └── TUserMapper.java ├── service │ ├── ITOrderService.java │ ├── ITGoodsService.java │ ├── ITUserService.java │ └── impl │ ├── TOrderServiceImpl.java │ ├── TGoodsServiceImpl.java │ └── TUserServiceImpl.java └── resources/mapper ├── TOrderMapper.xml ├── TGoodsMapper.xml └── TUserMapper.xml批量生成最大的好处就是一次把所有相关文件生成齐全,不用逐个表跑。三张表对应3个实体、3个Mapper、3个Service、3个Controller,加起来十多个文件,几秒钟搞定。相比之下手工写不是写不完,而是每张表都要重复同一套模板式操作,时间成本高且容易审美疲劳。
3.4 打开生成的文件检查
生成完先别直接抄进项目,逐个文件扫一遍很重要。实体类是基础,长这样:
@TableName("t_user") public class TUser { @TableId(value = "id", type = IdType.AUTO) private Long id; @TableField("username") private String username; @TableField("age") private Integer age; private String email; }这里能看到几个关键点:表名映射正确去掉了前缀,主键字段标注了自增策略,非主键字段通过@TableField与数据库字段对应。Service接口继承了MyBatis-Plus的IService,ServiceImpl继承了ServiceImpl,这意味着基础CRUD方法全部内置,不需要自己实现。
Controller默认生成了一组标准接口,包含分页列表、详情、保存、更新、删除。分页列表方法的写法类似:
@GetMapping("/page") public IPage<TUser> page(@RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size) { Page<TUser> page = new Page<>(current, size); return userService.page(page); }到这里,一套可以直接启动的CRUD骨架就就位了。不过骨架归骨架,真落到业务里还有几件事必须改造,下面展开讲。
4. 生成代码别急着用:落地改造的三个重点
4.1 公共字段自动填充,省掉一半重复代码
业务表里最常见的两个公共字段是create_time和update_time。如果每次保存、更新都手动set,不仅代码丑,还容易漏。MyBatis-Plus提供了MetaObjectHandler机制,可以自动填充这两个字段。
定义一个handler类:
@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()); } }实体类中对应字段加上自动填充策略:
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;配置完成后,以后新增、更新操作完全不用再管这两个字段,框架自动写入时间。为了减少后续补这步的麻烦,可以在生成器策略里通过FieldFill接口给公共字段设置填充策略,但更稳妥的做法是先生成,再统一在一个地方补充handler和注解。因为自动填充属于通用逻辑,放在公共基类里比每张表单独处理更优雅。
4.2 无状态通用CRUD服务怎么设计
生成器生成的Service继承了IService,里面已经带了save、saveBatch、removeById、listByIds、page等方法。很多项目到这里就直接用了,但实际业务里除了Controller层,定时任务、消息队列消费者、命令行脚本也经常要做增删改查。如果每个地方都注入实体对应的Service,代码重复度还是高。更好的做法是抽一层无状态的通用CRUD服务。
无状态的意思是:服务实例不持有业务数据,不在成员变量里缓存查询结果,不保存会话上下文,所有方法都是传入参数、返回结果、调用结束立即释放。这样同一个服务可以被多个业务场景安全复用,不会被上一次调用的残留数据干扰。可以理解为档案室的抽屉,你报编号我取档案,取完就关抽屉,不会因为你上次看过什么而影响这次的结果。
通用CRUD工具类的设计可以这样:
@Service public class CommonCrudService<T> { @Autowired private BaseMapper<T> baseMapper; public T selectById(Serializable id) { return baseMapper.selectById(id); } public List<T> selectBatchIds(Collection<? extends Serializable> idList) { return baseMapper.selectBatchIds(idList); } public boolean insert(T entity) { return baseMapper.insert(entity) > 0; } public boolean insertBatch(Collection<T> entityList) { entityList.forEach(baseMapper::insert); return true; } public boolean updateById(T entity) { return baseMapper.updateById(entity) > 0; } public boolean deleteById(Serializable id) { return baseMapper.deleteById(id) > 0; } public List<T> list(Wrapper<T> queryWrapper) { return baseMapper.selectList(queryWrapper); } }这个类的核心价值在于直接操作BaseMapper,不依赖具体业务Service。方法全部是纯函数式的:输入参数、输出结果、内部无状态。批量插入的时候要注意,如果list非常大,一次性遍历插入数据库连接压力大,建议按每批200到500条切割循环处理:
public void insertBatchLarge(Collection<T> entityList) { List<T> list = new ArrayList<>(entityList); int batchSize = 500; for (int i = 0; i < list.size(); i += batchSize) { List<T> subList = list.subList(i, Math.min(i + batchSize, list.size())); subList.forEach(baseMapper::insert); } }当然,如果数据量巨大,直接用ServiceImpl自带的saveBatch配合分段提交更高效。MyBatis-Plus的saveBatch方法默认batchSize是1000,在批量操作时是很实用的内置能力。
4.3 Controller返回结构统一
生成器生成的Controller直接返回实体对象或IPage,这在内部系统够用,但对外提供API时,统一返回结构迟早要做。常见的做法是定义一个BaseResult或Result类,包装code、message、data三个字段,然后把Controller里的返回类型改成Result。手动改几十个Controller确实麻烦,有精力的团队可以直接改代码生成器的模板文件,把Controller模板里的返回类型统一替换成Result包装。这也是代码生成器灵活性的体现:模板不是写死的,Freemarker模板文件可以按团队规范调整。
5. 常见问题与排查技巧实录
5.1 生成完找不到文件
最常见的原因是outputDir没设置或用了相对路径。我见过不止一个同事跑完生成器,在项目目录里翻半天找不到产物,最后发现文件生成到了系统的临时目录。这个问题没有太多技术含量,但特别消磨心情。解决办法就是按前面写的,outputDir一律用绝对路径,指定到项目的src/main/java目录。生成之前确认路径存在,否则部分版本不会主动创建目录也会导致异常。
5.2 重复生成不生效
表结构调整之后重新跑生成器,发现实体没变化。先把globalConfig里的enableFileOverride开了再说。FastAutoGenerator的默认行为是跳过已存在的文件,这个设计是为了防止误覆盖,但开发阶段如果需要反复调整,不开这个开关就会觉得生成器“没反应”。改表结构前先确认这个开关是开的,能省不少时间。
5.3 MySQL连接相关报错
运行生成器时遇到“Public Key Retrieval is not allowed”,是MySQL 8.x的常规问题,连接串加上allowPublicKeyRetrieval=true&useSSL=false即可。遇到“Could not create connection to database server”,优先检查时区参数serverTimezone是否配置。遇到“Table not found”之类的错误,检查表名是否写错,注意大小写敏感问题。
我整理了一份问题速查表,基本覆盖了日常使用中的高频故障。
| 症状 | 常见原因 | 处理办法 |
|---|---|---|
| 生成后找不到文件 | outputDir未设置或用了相对路径 | 显式配置绝对路径到src/main/java |
| 重复生成不更新 | 默认跳过已存在文件 | globalConfig开启enableFileOverride |
| Public Key Retrieval is not allowed | MySQL 8公钥获取限制 | URL加allowPublicKeyRetrieval=true&useSSL=false |
| 实体类没有Lombok注解 | 策略中未开启Lombok | entityBuilder().enableLombok() |
| Controller是@Controller不是@RestController | 未开启REST风格 | controllerBuilder().enableRestStyle() |
| Mapper XML扫描不到 | XML路径与配置不匹配 | 项目配置mybatis-plus.mapper-locations=classpath*:mapper/**/*.xml |
| 中文字段注释乱码 | 连接串没有指定编码 | URL加characterEncoding=utf8 |
5.4 生产级项目的小建议
版本升级到3.5.x之后,生成器配置和运行基本稳定,但有几个小习惯建议养成分。生成器主类放独立目录,不要放业务包,最好标记为工具类,因为在生产环境打包时不需要跑它。生成完的代码先编译一次再提交,避免把错误文件推进仓库。生成器不需要每次启动项目都跑,它只是开发阶段的一次性工具,建好表后跑一次就够了,以后表结构变更再重新执行。
最后再分享一个我实际项目里的经验。如果团队有多个人都需要用到代码生成器,不要让每个人都配一套生成规则,而是由一个人把生成器主类配置好,commit到仓库里,其他人拉代码后只改数据库密码就能直接用。这样所有人生成出来的代码风格完全一致,code review时也不用再因为个人习惯不同而争论格式问题了。
我个人最开始接触代码生成器时也觉得可有可无,但用习惯了之后,它的收益体现在长期维护上。项目里表越多、模块越碎片化,生成器带来的节省越明显。新表进来改一下addInclude的表名列表,跑一下main方法,一套标准代码直接落盘,剩下的时间全部留给业务逻辑。这种投入产出比,值得每个用MyBatis-Plus的Java后端同学花上一下午把这套流程搭起来。