1. 为什么"代码生成器"这件事值得重新拿出来聊
第一次接触 generate-code 这个项目,是在一个后台管理系统的迭代周期里。当时团队面临的情况很典型:数据库表结构已经稳定,接口文档写了两轮,但真正落到代码层面,Controller、Service、Mapper、DTO、VO 这一整套东西,每张表都要重复写一遍。一个中等规模的业务模块,十几张表,光是把增删改查的骨架敲完,就得耗掉两三天。更麻烦的是,这种重复劳动带来的不是"累",而是"错"——字段类型对不上、命名风格不统一、分页参数漏传,这些问题在联调阶段集中爆发,排查成本远高于写代码本身。
generate-code 就是在这个背景下进入视野的。它是 FlierTeam 开源的一套代码生成器,核心定位很明确:把"从数据库表结构到可运行代码"这段机械流程自动化。你给它一张表,它给你一套符合工程规范的代码骨架,包括实体类、数据访问层、业务层、接口层,甚至前端页面模板。听起来像是老生常谈的"低代码"概念,但实际用下来会发现,它和那些拖拽式平台完全是两条路线——它不接管你的运行时,只帮你把重复的"打字工作"干掉,生成出来的代码是标准的、可读的、可继续手改的源码,而不是一堆黑盒配置。
这篇文章适合几类人看:一是正在维护中后台系统、被 CRUD 淹没的后端开发者;二是想了解代码生成器内部到底怎么运转、想自己改造模板的技术负责人;三是对"元数据驱动开发"这个思路感兴趣、想找个轻量级项目练手的同学。我会从它的核心机制讲起,拆解模板引擎的工作方式,然后给出完整的落地步骤,最后重点聊那些文档里不会写、只有真正跑过一遍才会遇到的坑。全程按我自己的实操顺序来,不堆概念,只讲能复现的东西。
2. generate-code 的核心机制:元数据、模板与渲染三段式
2.1 它到底"生成"的是什么
很多人对代码生成器的第一印象是"一键出全套",但实际用起来必须先搞清楚它的产出边界。generate-code 的产出是源码文件,不是可执行程序,也不是数据库脚本。它的输入是表结构元数据,输出是一组按目录组织好的代码文件。中间的过程,是把元数据喂给模板,模板渲染出文本,文本落盘成文件。
这个链条里,最容易被低估的是"元数据"这一环。表名、字段名、字段类型、注释、主键、是否可空、默认值——这些信息决定了生成代码的质量上限。如果数据库里的字段注释是空的,生成出来的实体类就没有字段说明,后续维护的人只能靠猜。所以我在实际项目里养成了一个习惯:建表时把注释写全,这不是为了好看,而是为了让生成器有足够的"原料"。字段注释、表注释、枚举值的说明,这些在 generate-code 里都会被读取并映射到代码注释中。
另一个需要明确的是,generate-code 生成的是骨架,不是完整业务逻辑。比如一个查询接口,它会生成分页参数、查询条件对象、调用 Service 的骨架,但具体的业务过滤规则、权限校验、数据脱敏,这些还是得你自己填。把它当成"帮你写完 70% 的样板代码",而不是"帮你写完整个功能",心态会正很多。
2.2 模板引擎:整个项目的发动机
generate-code 的核心是模板引擎。它的工作方式可以用一个生活化的类比来理解:模板就像是一个"填空题的试卷",元数据就是"答案",渲染过程就是"把答案填进试卷的空格里"。试卷的格式是固定的,但答案千变万化,所以同一套模板能生成无数张不同的表对应的代码。
模板文件通常是文本文件,里面混着两种内容:一种是静态文本,比如public class、@Override这些固定不变的代码结构;另一种是占位符和逻辑指令,比如{{className}}、{{#if hasCreateTime}}这种,渲染时会被替换成实际值或根据条件决定是否输出。generate-code 的模板体系支持条件判断、循环、字段遍历,这意味着你可以写出"如果表里有 create_time 字段,就生成对应的审计逻辑"这种智能模板。
我在改造模板时踩过一个坑:模板里的空白字符处理。模板文件为了可读性,通常会缩进、换行,但渲染出来的代码如果直接保留这些空白,就会出现大量空行和错位缩进。generate-code 在渲染时对空白有一定处理,但不同模板引擎的行为不一样。我的经验是,模板里尽量用"逻辑指令独占一行"的写法,渲染后再用格式化工具(比如 IDE 的 Reformat Code)统一处理一遍,这样出来的代码才干净。
2.3 元数据从哪来:数据库反向工程
generate-code 获取元数据的方式是连接数据库,读取表结构。它会查询系统表(比如 MySQL 的information_schema),拿到表的字段列表、类型、注释、索引等信息,然后转换成内部的元数据模型。这个过程叫"反向工程"。
反向工程有几个细节值得注意。第一是类型映射:数据库的varchar、int、datetime、decimal这些类型,要映射成 Java 的String、Integer、LocalDateTime、BigDecimal。这个映射规则在 generate-code 里是可配置的,不同数据库的映射表不一样。第二是命名转换:数据库字段通常是下划线命名(user_name),Java 属性是驼峰命名(userName),这个转换规则也要配置好,否则生成出来的代码风格会很别扭。第三是主键识别:单主键、复合主键、自增主键,处理方式不同,generate-code 会根据主键类型决定生成insert时是否包含主键字段。
我个人的做法是,在正式生成之前,先用 generate-code 的"预览"功能看一眼元数据解析结果。如果发现某个字段的类型映射不对,或者注释没读出来,先解决元数据问题,再生成代码。元数据错了,后面全错,这个顺序不能颠倒。
3. 从零跑通 generate-code:环境、配置与第一次生成
3.1 环境准备里最容易被忽略的两件事
generate-code 是开源项目,通常以源码形式提供,需要自己编译运行。环境准备这一步,大部分人卡在两个地方:JDK 版本和数据库驱动。
JDK 版本方面,generate-code 这类项目一般基于 JDK 8 或 JDK 11 开发。如果你的机器上装的是 JDK 17 或更高版本,可能会遇到模块化相关的报错,比如module java.base does not "opens java.lang"。解决办法是在启动参数里加上--add-opens相关的配置,或者干脆用 JDK 8/11 跑。我建议用 JDK 8,因为这类工具类项目对高版本 JDK 的适配往往不完整,用低版本能省掉一堆麻烦。
数据库驱动方面,generate-code 需要连接你的目标数据库来读取表结构。如果你用的是 MySQL,需要把 MySQL 的 JDBC 驱动放到项目的依赖里。这里有个细节:驱动版本要和数据库版本匹配。MySQL 5.7 和 8.0 的驱动类名不一样(com.mysql.jdbc.Drivervscom.mysql.cj.jdbc.Driver),连接 URL 的参数也有差异(比如时区配置serverTimezone)。我见过有人因为驱动版本不对,连了半天连不上数据库,最后发现是 URL 里少了个时区参数。
提示:环境准备阶段,先用一个最简单的数据库连接测试跑通,确认能读到表列表,再往下走。不要一上来就配置全套模板,那样出问题时很难定位是环境问题还是配置问题。
3.2 配置文件:把"生成什么"和"生成到哪"说清楚
generate-code 的配置文件通常包含几块内容:数据库连接信息、代码输出路径、包名配置、模板选择、作者信息等。这些配置看起来琐碎,但每一项都直接影响生成结果。
数据库连接信息不用多说,重点是输出路径和包名。输出路径决定了生成的文件落在哪个目录,包名决定了生成代码里的package声明和目录结构。这两者必须对应上,否则生成出来的代码放到项目里会报"包名与路径不匹配"的错误。我的做法是,先在项目里建好目标目录结构(比如src/main/java/com/example/module),然后把输出路径指向这个目录的根,包名配置成com.example.module,这样生成的文件直接就能用。
模板选择是另一个关键点。generate-code 通常提供多套模板,比如"单表 CRUD 模板"、"树形结构模板"、"前后端一体模板"。不同模板生成的代码结构不一样,选错了要么生成一堆用不上的文件,要么缺少关键文件。我建议第一次使用时,先用默认模板生成一张简单的表,看看产出物长什么样,再决定要不要换模板或改模板。
作者信息这块,配置里通常有author字段,会写进生成代码的类注释里。这个字段建议填团队名或项目名,不要填个人昵称,因为代码是要进版本库的,个人化信息容易引起混淆。
3.3 第一次生成:选一张"干净"的表试水
第一次生成,不要拿核心业务表开刀。选一张结构简单、字段少、没有复杂关联的表,比如"字典表"或"配置表"。这样做的目的是验证流程,而不是产出可用代码。
生成过程通常是:在 generate-code 的界面或命令行里,选择数据库、选择表、点击生成。生成完成后,去输出目录看文件。重点检查几件事:文件是否都生成了(实体类、Mapper、Service、Controller 是否齐全)、包名是否正确、字段类型映射是否合理、注释是否带上了。
我第一次生成时,发现实体类里的Date字段被映射成了java.util.Date,而项目里统一用的是java.time.LocalDateTime。这就是类型映射配置没改。后来我在配置里把datetime的映射改成了LocalDateTime,重新生成,问题解决。这个经历告诉我:第一次生成的核心目的是暴露配置问题,所以生成后一定要仔细看,不要生成完就直接往项目里拷。
4. 模板改造:让生成代码真正贴合你的工程规范
4.1 为什么默认模板往往"差一口气"
generate-code 的默认模板是通用型的,它要照顾各种项目风格,所以不可能完全贴合你的团队规范。常见的"差一口气"的地方包括:命名风格不一致(比如默认用selectById,你们团队用getById)、缺少统一响应包装(比如默认返回实体,你们要求返回Result<T>)、缺少审计字段处理(比如create_time、update_time的自动填充)、缺少参数校验注解(比如@NotNull、@Valid)。
这些问题不解决,生成出来的代码就得手动改一遍,那生成器的价值就打折了。所以模板改造是让 generate-code 真正好用的关键一步。改造的目标不是"改得面目全非",而是"让生成结果直接能提交,不需要二次修改"。
4.2 改造模板的实操路径
改造模板之前,先做一件事:收集三到五份团队里公认写得好的同类代码。比如找一个标准的 Controller、一个标准的 Service、一个标准的实体类,把它们放在一起对比,找出共同的结构特征。这些特征就是模板要固化的东西。
然后打开 generate-code 的模板文件,对照着改。模板文件通常是.vm(Velocity)或.ftl(FreeMarker)格式,语法不复杂,核心就是占位符替换和条件判断。改造时按优先级来:
第一优先级是包名和导入。确保生成的代码导入的是你项目里实际使用的类,而不是模板作者假设的类。比如统一响应类、分页类、工具类,这些的包路径要改对。
第二优先级是注解。实体类上的@TableName、@TableId,Controller 上的@RestController、@RequestMapping,Service 上的@Service,这些注解的写法要和项目一致。参数校验注解、Swagger 注解(如果项目在用)也要加上。
第三优先级是方法结构。比如 Controller 里的方法,是直接返回Result.success(data),还是返回ResponseEntity,这个要按项目规范来。Service 里的方法,是接口和实现分离,还是直接一个类,也要按项目结构来。
第四优先级是注释和作者信息。类注释、方法注释的格式,作者字段的取值,这些虽然不影响运行,但影响代码的可维护性。
改造完模板后,一定要用同一张表重新生成一遍,对比改造前后的差异。我一般会生成两次,一次用默认模板,一次用改造后的模板,把两份代码并排看,确认改造后的版本确实更贴合项目规范。
4.3 模板改造中的一个反直觉经验
很多人改造模板时,倾向于"一次改到位",把所有能想到的规范都塞进去。我的经验恰恰相反:模板改造要小步走,每次只改一类东西,改完验证一次。
原因很简单:模板是文本,改错一个字符就可能导致渲染失败,或者生成出语法错误的代码。如果一次改太多,出问题时你根本不知道是哪处改动引起的。我自己的做法是,先改包名和导入,生成验证;再改注解,生成验证;再改方法结构,生成验证。每一步都确认没问题了,再进入下一步。这样虽然看起来慢,但总体效率更高,因为排查成本低。
另一个经验是:模板里不要写太复杂的逻辑。模板引擎支持条件判断和循环,但不适合写复杂的业务逻辑。比如"如果字段名是 xxx 且类型是 yyy 且注释包含 zzz,就生成某种特殊代码"——这种逻辑写在模板里,过两个月你自己都看不懂。模板应该保持"薄",复杂的判断逻辑放到生成器的配置里,或者干脆生成后手动处理。
5. 生成之后的收尾工作:那些生成器不管的事
5.1 代码格式化与静态检查
generate-code 生成出来的代码,虽然结构是对的,但格式往往不够整齐。缩进、空行、换行位置,这些细节模板很难完全控制。所以生成后的第一件事,是用 IDE 的格式化功能统一处理一遍。IntelliJ IDEA 的Reformat Code(快捷键Ctrl+Alt+L)可以批量格式化整个目录,几秒钟就能让代码变得整齐。
格式化之后,跑一遍静态检查。如果项目用了 Checkstyle、SpotBugs 或 SonarLint,生成代码可能会触发一些告警,比如"魔法值"、"未使用的导入"、"方法过长"。这些告警不一定要全部消除,但要过一遍,确认没有真正的隐患。我遇到过生成代码里带了未使用的导入,虽然不影响运行,但提交时会被 CI 拦下来,所以提前处理掉比较省事。
5.2 数据库脚本与代码的同步
generate-code 只生成代码,不生成数据库迁移脚本。但实际项目里,代码和数据库结构必须同步。所以生成代码之后,要确认数据库里确实有对应的表,字段类型和代码里的映射一致。
如果项目用了 Flyway 或 Liquibase 这类数据库迁移工具,建议把建表脚本也纳入版本管理,和生成的代码一起提交。这样别人拉下代码后,跑一遍迁移脚本就能建好表,再跑一遍生成器就能得到一致的代码。代码和脚本分离管理,是很多团队后期混乱的根源,这一点值得提前注意。
5.3 生成代码的"二次加工"边界
generate-code 生成的代码是骨架,业务逻辑要自己填。但"填多少"是有讲究的。我的原则是:生成器生成的部分,尽量不改;业务逻辑,尽量写在单独的方法或类里。
举个例子,生成的 Service 类里有一个page方法,负责分页查询。如果你直接在page方法里加业务过滤条件,那下次重新生成时,这些改动就会被覆盖。更好的做法是,在 Service 里加一个buildQueryWrapper方法,把过滤逻辑写在那里,page方法调用它。这样即使重新生成,业务逻辑也不会丢。
这个原则说起来简单,做起来需要一点自律。但养成习惯后,你会发现生成器的价值大大提升——因为它生成的代码可以随时重新生成,而你的业务逻辑始终安全。
6. 踩坑实录:那些文档里不会写的细节
6.1 字段名与 Java 关键字的冲突
数据库字段命名有时候会撞上 Java 关键字。比如有个字段叫class,或者interface,或者default。generate-code 在生成实体类属性时,如果直接把这些字段名转成属性名,生成的代码就编译不过。
我遇到过一次,一张表里有个字段叫order,生成的实体类里有个属性叫order,结果和 SQL 里的order by冲突,运行时报语法错误。解决办法是在配置里加一个"关键字转义"规则,比如把order转成orderField,或者在模板里加判断,遇到关键字就加前缀。
这个坑的隐蔽性在于:生成阶段不报错,编译阶段也不报错,只有运行时才暴露。所以生成代码后,最好扫一眼字段名,看看有没有可疑的关键字。
6.2 注释里的特殊字符导致渲染失败
模板渲染时,如果元数据里的注释包含特殊字符(比如#、$、{}),可能会被模板引擎当成语法解析,导致渲染失败或输出错乱。我遇到过一次,表注释里有个#号,结果生成的类注释从#号开始的内容全丢了。
解决办法有两个:一是在模板里对注释字段做转义处理,二是建表时避免在注释里用特殊字符。我倾向于后者,因为转义规则在不同模板引擎里不一样,容易出错。建表注释就用纯文本,别加花哨的符号。
6.3 重新生成时的文件覆盖问题
generate-code 重新生成时,默认行为是覆盖同名文件。这意味着如果你在生成的代码里改了东西,重新生成后这些改动就没了。这个坑我踩过不止一次。
应对策略有三层:第一层是前面说的"业务逻辑写在单独方法里",减少对生成代码的直接修改;第二层是生成前先提交一次 Git,这样即使覆盖了也能找回;第三层是如果 generate-code 支持"增量生成"或"跳过已存在文件"的配置,把它打开。三层叠加,基本能避免"改了半天被覆盖"的悲剧。
6.4 多模块项目里的路径问题
如果项目是多模块的(比如api模块、service模块、dao模块分开),generate-code 的输出路径配置会更复杂。你需要为不同类型的代码配置不同的输出路径:实体类输出到dao模块,Service 输出到service模块,Controller 输出到api模块。
这个配置如果搞错了,生成的文件会全部堆在一个目录里,然后你得手动搬运。我的做法是,在配置文件里把路径写成变量,比如${module.dao.path}、${module.service.path},然后在不同的生成任务里传入不同的值。这样一套模板可以适配多个模块,不用为每个模块单独改配置。
7. 我对 generate-code 这类工具的真实看法
用了一段时间 generate-code 之后,我对代码生成器的态度从"可有可无"变成了"中后台项目的标配"。原因不在于它能省多少打字时间,而在于它强制统一了代码风格。一个团队里,每个人写 CRUD 的习惯都不一样,有人喜欢把逻辑写在 Controller 里,有人喜欢抽到 Service,有人用@Autowired,有人用构造器注入。生成器把这些差异抹平了,出来的代码结构一致,后续维护的人不用在几种风格之间切换。
但它也不是银弹。generate-code 解决的是"从表到代码"这一段,解决不了"从需求到表"这一段。表结构设计得好不好,字段拆分得合不合理,这些还是靠人。而且生成器生成的代码是"平均水准"的代码,它不会帮你写出优雅的业务逻辑,只是帮你把地基打好。
我的建议是:把 generate-code 当成一个"代码规范执行器"来用,而不是"开发效率工具"。它的最大价值不是快,而是一致。当你把模板改造得足够贴合团队规范后,它生成的代码可以直接提交,不需要 review 风格问题,review 的人可以专注于业务逻辑本身。这才是它真正省时间的地方。
最后分享一个小技巧:generate-code 的模板文件建议纳入版本管理,和项目代码一起提交。这样模板的每一次改动都有记录,新人入职时也能看到"为什么模板长这样"。模板是团队的资产,不是某个人的临时配置,把它管起来,价值会持续放大。