做后端开发的朋友,一定都经历过那种"复制、粘贴、改字段名"的机械化时刻。尤其是Spring Boot、Spring Cloud这类微服务项目,一个服务模块动辄几十张表,每张表都要写Entity、Mapper、Service、Controller,翻来覆去就是那些套路化代码。今天要聊的"代码生成器",就是专门解决这个问题的。它不是那种只能跑Demo的玩具,而是能直接落到你项目里、真正提升开发效率的利器。这篇文章我会从选型思路、配置拆解、实操步骤到踩坑记录,完整讲一遍。
1. 为什么写代码生成器:先搞清楚它在解决什么问题
1.1 开发中最容易被浪费的时间
我见过很多新同事,入职第一周干得最多的事情就是把一张表的CRUD代码照着另一张表改一遍。改完Entity改Mapper,改完Mapper改XML,改完XML再改Service,最后Controller里那几个方法几乎原封不动。这里面的问题不是"不会写",而是"没必要写"。这些代码有固定结构、固定命名、固定套路,属于典型的样板代码。
尤其是Spring Cloud项目,服务拆得细,表结构却未必少。一个订单服务、一个用户服务、一个支付服务,每个里面都有十几张核心表。如果靠手写,这些重复劳动会吃掉大量时间。写的人手疼,review的人眼疼,改字段的时候更是全员头大。把这种劳动交给代码生成器,是我个人认为投入产出比最高的优化手段。
1.2 代码生成器的边界:别指望它生业务逻辑
说一下代码生成器能做什么、不能做什么。它能帮你把围绕单表的增删改查、分页查询、批量操作的骨架代码全部生成,包括与数据库表映射的实体类、数据访问层接口和XML、业务层接口与实现类、控制层接口。它不能做的是业务逻辑——比如订单状态机怎么流转、库存怎么冻结和释放、退款流程怎么编排,这些还是得人写。
这个边界想清楚很重要。很多团队用代码生成器觉得"不好用",是因为试图让它生成完整的业务模块。实际上它解决的是"表结构到基础代码"这一段,业务逻辑基于生成的骨架继续扩展。这就像盖房子,生成器相当于把毛坯和水电管线弄好,装修方案还得自己定。把预期放在正确的位置,这工具就好用了。
1.3 适合谁用、用在哪里
从我的经验来看,这几类场景收益最明显:一是新项目启动初期,数据库模型已经初步设计好了,需要快速铺出可运行的后端服务;二是Spring Cloud微服务改造中,每个子服务都要建一套标准的代码结构;三是老系统迁移升级,旧的DAO层写法五花八门,想统一规范。
有人可能会问,项目都已经做起来了再引入生成器会不会很麻烦?我的建议是,不需要一次性把全项目都重新生成,只需要在其中几个新模块或者新表上用起来,慢慢过渡。生成器本身是个工具,不是架构改造,随时停用也不会影响已有代码。
2. 代码生成器方案选型:站在巨人的肩膀上干活
2.1 生态里的主流选择
Java后端的代码生成器方案其实不少。最初接触的是MyBatis官方提供的Generator工具,它能根据数据库表生成实体类、Mapper接口和XML文件。这个方案很纯粹,但生成的内容比较"素",Service、Controller都得自己搭,而且XML里的配置写起来也比较繁琐。
后来很多企业级框架都内置了自己的生成功能,比如一些快速开发平台,在界面上选表、点生成,整套代码就出来了。这种方案确实省事,但耦合性比较强,如果项目不是基于这套平台开发的,引入它就要连框架一起引入,代价比较大。
目前社区里用的比较多、口碑最好的,是MyBatis-Plus这个开源增强框架自带的代码生成器。它实际上把MyBatis Generator做了大幅度的封装和增强,支持各种自定义模板引擎,能生成Controller、Service、ServiceImpl、Mapper、XML、Entity全套,而且和Spring Boot的整合体验很好。
2.2 为什么我最终选了这套方案
选型不能光看功能多少,要看它是否能优雅地嵌入现有技术栈。我的项目本身用的是Spring Boot + MyBatis-Plus这套组合,所以选它的原生生成器几乎零成本。
另一个重要原因是模板可定制。生成器生成的代码结构由Freemarker或Velocity模板控制,团队如果要做统一的代码风格、加统一的注释头、生成统一的返回对象R包装,这些都是可以通过改模板实现的。这种可定制能力让生成器不再是"一次性工具",而是可以长期维护的团队基建。
还要看社区活跃度。用过技术方案的朋友都懂,社区不活跃的工具,出了问题排查起来非常痛苦。MyBatis-Plus本身用户基数大,生成器相关的问题在搜索平台上基本都能找到答案,文档也很全,这对团队内部推广来说是非常关键的一点。
2.3 一个重要的提醒:生成器不等同于代码框架
选型时容易犯的错是,把代码生成器和Web开发框架混为一谈,觉得用了生成器,项目脚手架就自动搭好了。实际上,生成器解决的是"单表CRUD代码"的产出问题,而项目本身的依赖管理、统一配置中心、网关路由、服务注册发现这些东西,还是需要团队自己搭建或使用已有的公司级脚手架。
我看到一些团队引入生成器后很兴奋,觉得可以解决一切重复劳动。冷静下来会发现,它只是把最底层、最没有技术含量的部分自动化了,项目的核心架构设计、公共组件封装、异常处理体系,仍然需要踏踏实实去建设。把工具放在正确的位置,它才能发挥真正的价值。
3. 核心功能拆解:生成器到底能生成什么、怎么配置
3.1 生成产物全景
先看一套标准的生成结果包含哪些文件。假设数据库里有一张用户表t_user,生成器会输出至少七类文件:
| 产物 | 说明 | 典型文件名 |
|---|---|---|
| 实体类 | 与表字段一一对应的POJO,包含注解映射 | User.java |
| Mapper接口 | 数据访问层接口,内置常规CRUD方法 | UserMapper.java |
| XML文件 | 复杂SQL存放位置,生成基础结果映射 | UserMapper.xml |
| Service接口 | 业务层接口定义 | UserService.java |
| Service实现 | 业务层默认实现类 | UserServiceImpl.java |
| Controller | Web层接口,默认生成Restful风格方法 | UserController.java |
| DTO/VO | 可选,部分配置模板会生成入参出参对象 | UserQuery.java等 |
这里面的核心要点是,生成的Controller和Service并不是"能用就行"的凑合产物。配置得当的情况下,生成出来的Controller会统一封装返回结构,比如统一使用项目里的Result对象;Service里会做好通用分页参数的接收;XML里会生成好数据库字段和Java字段的自动映射规则。也就是说,生成结果不仅仅是"有",而是"符合团队规范"。
3.2 全局配置与数据源配置
全局配置是整个生成流程的地基,主要包括输出目录、作者名、是否生成Swagger注解、日期格式处理等。我在实际项目中是这样配置的:
- 输出目录:指定到具体业务模块的源码根目录,比如
.../user-service/src/main/java - 作者名:统一使用团队标识,而不是个人名字,这样后续看到代码能知道是生成产物
- 日期类型:数据库的datetime类型映射为Java的LocalDateTime,这个一定要开,否则默认映射成java.util.Date,LocalDateTime在配合Jackson序列化时会更顺手
- Swagger注解:项目里如果用了Knife4j或SpringDoc,开启后每个字段都会生成对应的Schema注解
数据源配置没什么花头,就是数据库地址、用户名、密码。但有一点要注意,连接串里必须指定好useInformationSchema=true,这个参数在MySQL 8以上版本里很重要,它能让生成器正确读取到表注释和字段注释。不配的话,生成的代码里注释全都是空的,看着很别扭。
3.3 表与字段策略配置:决定代码规范的关键
策略配置是生成器里最有技术含量、也最需要仔细设置的部分。服务项目里表名、字段名的命名习惯和Java的驼峰命名是两套体系,比如数据库字段user_name,Java里应该写成userName,这块生成器默认会自动转换,但有几个点需要显式确认。
- 表前缀过滤:如果表名都是
t_开头,比如t_user、t_order,配置addTablePrefix("t_")后,生成的实体类名就是User而不是TUser - 主键策略:常用的主键策略有
ASSIGN_ID(雪花ID)和AUTO(数据库自增)。如果是分布式微服务系统,主键我强烈建议用雪花ID,避免分库分表后主键冲突 - 逻辑删除配置:很多业务表都有
deleted字段表示逻辑删除,配置logicDeleteFieldName后,生成的所有查询语句都会自动带上WHERE deleted = 0,生成的实体类上也会自动加@TableLogic - 乐观锁配置:在有并发更新的场景,比如订单余额表,配置好
version字段后,更新语句会带上WHERE version = ?以及SET version = version + 1
这些配置直接决定了生成的代码是否具备生产环境要求的健壮性。很多生成器生成的代码在Demo里跑得很欢,一上生产就出问题,主要就是这些细节没配到位。
3.4 模板引擎选择与包路径规划
模板引擎有Freemarker和Velocity两种选择,默认是Velocity。我两种都用过,实际体验差别不大,看团队熟悉什么。如果项目里已经用到了Freemarker(比如某些后台管理系统前端模板),生成器也统一用Freemarker,可以减少依赖。
包路径规划这块,我建议在Spring Cloud项目里避免把所有模块的生成代码放在同一个包下。正确做法是每个微服务模块单独一套包结构,比如com.company.user、com.company.order,包名与模块严格对应。生成器支持配置parent包名和moduleName,生成出来的完整包路径是parent + moduleName + 子包名。这样既能保证代码在各服务模块间清晰隔离,也方便后续做服务拆分。
4. 实操全程:从空工程到生成代码跑起来
4.1 第一步:准备一个生成器工程
生成器本身可以是一个独立的模块,不依赖业务代码。我的做法是在项目根目录下建一个generator模块,只用于执行代码生成,生成完后这个模块不参与线上部署。
Maven依赖长这样:
<dependencies> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.5</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> <version>8.0.33</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-extension</artifactId> <version>3.5.5</version> </dependency> </dependencies>版本号是我目前项目里实测稳定的组合。mybatis-plus-generator从3.5.3版本之后,把许多内部依赖拆分出来了,如果只引generator不加extension,运行时会报一些类找不到的错。直接把这几个依赖全部加上,能少折腾很多。
4.2 第二步:写生成器主流程代码
生成器的主流程代码非常简洁,核心就是构建各项配置。我直接用最新版推荐的FastAutoGenerator方式,比老版AutoGenerator少了大量setter调用。
关键配置我加了详细注释,直接复用即可:
public class MysqlGenerator { public static void main(String[] args) { FastAutoGenerator.create( "jdbc:mysql://localhost:3306/order_center?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&useInformationSchema=true", "root", "your_password" ) .globalConfig(builder -> { builder.author("team-dev") // 作者名,用团队标识 .enableSwagger() // 开启swagger注解 .dateType(DateType.TIME_PACK) // 时间类型映射LocalDateTime .commentDate("yyyy-MM-dd") // 注释里的日期格式 .outputDir("/workspace/order-center/order-service/src/main/java"); // 输出到业务模块源码根目录 }) .packageConfig(builder -> { builder.parent("com.company.order") // 父包名 .moduleName("") // 如果不需要嵌套一层模块名,留空 .entity("entity") .service("service") .serviceImpl("service.impl") .mapper("mapper") .controller("controller"); }) .strategyConfig(builder -> { builder.addInclude("t_order", "t_order_item") // 要生成哪几张表 .addTablePrefix("t_") // 表前缀过滤 .entityBuilder() .enableLombok() // 使用Lombok简化实体 .logicDeleteColumnName("deleted") // 逻辑删除字段 .versionColumnName("version") // 乐观锁字段 .controllerBuilder() .enableRestStyle(); // 生成@RestController }) .templateEngine(new FreemarkerTemplateEngine()) .execute(); } }启动main方法后,控制台会输出每一张表的生成进度。执行完毕后,到输出目录里就能看到完整的一套代码。
4.3 第三步:验证生成结果
生成不是终点,验证才是。我每次生成完第一步检查的是Entity类,打开生成的Order.java,重点看三处:表名注解是否正确映射、类型是LocalDateTime还是旧的Date、逻辑删除和乐观锁注解有没有正确贴到字段上。
第二步检查Controller,确认类上标的是@RestController,方法上用的是@GetMapping、@PostMapping这类组合注解而不是@RequestMapping。第三步是实际启动服务,用一个已有菜单或直接调接口验证增删改查是否正常。这里特别提醒一下,生成代码跑不起来,九成都是Mapper接口扫描路径的问题。如果你的Spring Boot启动类上的@MapperScan写的包路径和生成出来的mapper包路径不一致,接口是永远不会被扫描到的。别问我怎么知道的,这个问题几乎每个用生成器的项目都会遇到一次。
4.4 在Spring Cloud多模块项目中如何分发代码
Spring Cloud项目通常是一个父工程带多个子模块。生成器在不同模块间使用,有几个经验值得分享。最稳妥的方式是每个业务模块对应的表单独执行一次生成,比如要生成订单服务相关的表,outputDir就指向订单模块的路径;要生成用户服务的表,就改一下输出目录和数据源,再跑一次。
不建议把多张不同服务的表一次性生成到同一个目录里。表名可能在不同模块间重复,同时生成的代码如果落到同一个module下,反而制造了模块间的耦合,违背了微服务拆分的初衷。
另外,生成的代码需要纳入Git版本管理。不要在服务器上临时跑生成器然后把代码拷贝来拷贝去,生成器作为开发工具,代码生成后直接提交到代码仓库,和手写的代码同等对待,这样整个团队的代码基线才能保持一致。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 生成的实体类没有字段注释 | JDBC连接串缺少useInformationSchema=true | 在数据库URL末尾追加该参数并重启生成 |
| 控制台输出中文乱码 | 生成器工程和控制台的编码不一致 | IDE统一设置为UTF-8,Maven编译也指定UTF-8 |
生成的Controller方法全是/save、/list不带前缀 | 未配置API前缀或表字段无相关注解 | 使用@RequestMapping手动在类上加前缀 |
| 生成的XML提示找不到Mapper方法 | 生成的Mapper接口方法与XML中id不匹配 | 删除旧XML重新生成,确保两套文件同步 |
| Mapper接口无法注入 | 启动类未扫描到Mapper包 | 检查@MapperScan路径与生成包路径是否一致 |
| 日期字段生成成了java.util.Date | 未设置dateType为TIME_PACK | globalConfig中配置DateType.TIME_PACK |
| 逻辑删除失效,查询仍带出已删除数据 | 未配置逻辑删除字段 | 在strategyConfig中指定logicDeleteColumnName |
5.2 生成文件被覆盖的防范策略
生成器在设计上默认已经有了一套覆盖逻辑,同一张表生成两次,之前生成的代码会被覆盖。这对单纯由生成器产出的文件来说没什么问题,但如果这个被覆盖的文件里你已经手写过业务代码,那损失就大了。
这个问题防范的核心思路是:把"生成的文件"和"手写的文件"分开存放。我的做法是生成后,如果某个Service实现类里要加业务逻辑,那就把这个实现类标记为自定义代码,下次生成前在策略里忽略这个文件类型。生成器提供了配置项,可以设置不覆盖已有文件或者跳过某张表的Controller、ServiceImpl生成。你还可以把生成器配置里"覆盖已有文件"的开关关掉,让它在遇到同名文件时自动跳过,保证手写代码不会被冲掉。
5.3 表结构更新的处理方式
项目开发到中期,表结构变更是最头痛的事情之一。加了个字段,或者改了个字段类型,重新生成一遍代码吧,Service里手写的逻辑还在,但实体类和Mapper可能会被重置。怎么办?
目前我用下来最舒服的方案是:表结构变更后,不重新跑一整轮生成,而是只对实体类单独生成一次,把这个文件替换掉。Service、Controller这些有手写逻辑的文件不动,Mapper里新增字段的映射如果缺了,手动在XML里补一下。这么做虽然不如全量重新生成来得"干净",但保护了手写逻辑不被破坏,收益明显更大。
5.4 模板定制的进阶玩法
默认生成的Controller里每个方法都比较"裸",直接返回实体类或者没有统一的返回结构。实际项目里通常有一个Result<T>这样的统一包装对象,那模板就需要定制。
MyBatis-Plus生成器允许覆盖默认模板。它的模板文件在jar包里,结构比较清晰,Controller模板主要在templates/controller.java.ftl。我改造过的最有价值的几个地方:一是把返回类型统一改成Result<?>;二是自动给分页查询加上@Validated注解和参数校验;三是在生成的实体类上增加统一继承BaseEntity,把创建时间、更新时间这些公共字段塞进去,避免每张表反复定义这几个字段。
改造模板的收益是持续的,因为每次生成都会带上这些规范。前期花一两个小时调模板,后期省下来的时间绝对不是这点投入能比的。
5.5 生成器本身要不要作为团队基建维护
代码生成器这个东西,用的人越多,越值得认真维护。我的建议是,把它作为一个独立的工具库放在团队代码仓库里,同事拉下来改下数据库配置就能用。如果团队人多,还可以在生成器基础上做一个简单的界面,让人选表、选模块、点按钮就能生成,进一步降低使用门槛。
另外,生成器的升级迭代要跟上团队技术栈的变化。比如团队从Swagger换成了SpringDoc,生成注解的方式就要改;返回结果从老式的R对象换成了新规范的Result,模板就要调。把生成器当成一个持续演进的内部工具,而不是写完就扔的一次性脚本,它才能真正成为开发流程里的一环。
我在实际使用中最深刻的体会是:代码生成器最大的价值不在于省掉的那几次敲键盘,而在于它让整个团队的代码风格被"焊死"在了一个标准模板里。新人写的CRUD和老同事写的CRUD读起来一个样,接手任何模块都不会因为代码风格不同而感到陌生。如果你正被Spring Boot、Spring Cloud项目里没完没了的样板代码困扰,花半天时间把生成器搭起来,绝对不亏。