1. 重复代码背后的维护成本:模板生成不是偷懒,是消灭“人工一致性”问题
如果你也跟我一样,在一个业务系统里维护过二三十个长得差不多的模块,那你对“模板代码生成工具”这几个字一定不陌生。前阵子我重构内部后台,光 Controller、Service、Mapper、XML 这套增删改查,手写了两遍就开始烦躁,写到第五遍时已经能闭着眼睛敲完。就是这种“熟练”最危险——人一旦在重复劳动里变熟练,就特别容易漏字段,更容易在第九个模块里把第八个模块的隐藏逻辑一起抄过来。
这个工具能解决的事情比很多人想象中宽。它不只是把一张数据库表变成一堆 Java 文件,它可以把“按既定规则重复产出代码”这件事,从手工操作变成可执行、可测试、可回滚的工程能力。适合谁用?后端团队做规范化接口模块、前端团队批量生成页面和路由、运维写配置片段、甚至嵌入式领域按寄存器表生成解析代码,都能在里面找到对应的玩法。我下面主要用 Java 后端这个最典型的场景展开讲,但思路完全能平移。
1.1 手写重复代码的痛点不在“手累”,而在“不一致”
很多人一提到代码生成,第一反应是“省时间”。但我的体感是,省时间只是副产品,核心价值是消除“人工一致性”问题。
举个例子,你写了十个模块,每个模块都有根据 ID 删除、根据 ID 查询、分页列表、新增、更新这五个接口。手写状态下,第一个模块用了统一返回体 R<T>,第五个模块可能因为当时图省事直接返回了实体对象,等到第十个模块又会写得和第一个不一样。这种差异不是因为开发水平不稳定,而是人的注意力没办法在低信息密度的重复代码里保持全程标准。
模板生成就不一样。接口签名、返回体的包裹方式、异常处理、日志埋点、参数校验注解,全部由模板文件统一控制。如果规范变化,比如公司要求所有分页接口多返回一个 totalPage 字段,你只需要改模板和对应的输出映射,所有模块一起更新。而手工去改几十个模块,改漏一个通常不是会不会的问题,而是哪次会的问题。
1.2 为什么现成工具和 AI 对话替代不了模板生成
说到代码生成,很多人会问:现在有 AI,为什么还要自己折腾一套工具?
我自己的判断是,不同的生成路线适用不同场景。现成的低代码平台适合在公司标准框架内快速搭后台,但一旦你的项目用了私有脚手架、特殊的数据权限逻辑或者历史遗留包名,低代码平台生成的代码往往很难落到你的工程目录里;云端“数据库表转 CRUD 代码”的网页服务,适合一次性 Demo,但它的命名规则、文件结构、注解风格和你团队规约大概率对不上。
AI 生成更适合“你不知道怎么写”的场景,比如查一个不熟悉的 SDK 调用方式,或者生成一小段边界清晰的业务逻辑。而模板生成解决的是“我已经知道怎么写、而且知道要写一模一样的五十遍”的场景。AI 生成的问题是结果不可完全预期,同一段提示词在不同上下文里可能给出不一样的结果,模板生成则是确定性的:同样的元数据输入,永远得到同样的输出。
这也是我最终自己维护一套模板代码生成工具的根本原因。模板本身不复杂,复杂的是怎么让你的生成规则可配置、可复用、可评审。它更像是一个“把规范固化成代码”的工程动作,而不是一个写代码的快捷方式。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 模板代码生成工具的三大核心要素:元数据、模板语法、落盘约定
想自己写模板代码生成工具,先不用急着找框架。你要想清楚三件事:用什么作为生成依据,模板怎么写,生成的文件落在哪里。这三件事对应到工程上就是元数据、模板语法和落盘约定。
2.1 把“变化的东西”和“不变的东西”分开
模板生成最重要的思想不是“套字符串”,而是把变化和不变分离。不变的是结构,比如 Controller 里“查询->调用 Service->返回结果”这个顺序;变化的是数据,比如表名叫 product、字段有 product_name、主键叫 id。
所以第一件事是设计一个稳定的元数据模型。我用得比较多的是 JSON 作为输入配置,因为它不像 Java 类那样需要编译,也不像 XML 那样有大量冗余标签。一个模型的常见结构包括表名、模块名、字段列表、字段注释、字段类型、是否主键、是否逻辑删除、是否自动填充之类。
给一个直观的输入例子:
json复制{
"tableName": "product",
"moduleName": "product",
"entityName": "Product",
"apiPrefix": "/product",
"fields": [
{ "name": "id", "type": "bigint", "comment": "主键", "primaryKey": true },
{ "name": "product_name", "type": "varchar", "comment": "商品名称" },
{ "name": "category_id", "type": "bigint", "comment": "分类ID" },
{ "name": "price", "type": "decimal", "comment": "价格" },
{ "name": "status", "type": "int", "comment": "状态" },
{ "name": "created_at", "type": "datetime", "comment": "创建时间" },
{ "name": "updated_at", "type": "datetime", "comment": "更新时间" }
]
}
字段列表里那些“是否主键”“是否删除”“是否自动填充”不是浮于形式的标签,它们会直接影响模板里的条件分支。比如更新语句默认不应该更新主键,插入语句默认不需要插入创建时间,这些都是靠元数据里的属性来区分的。
2.2 占位符只是第一步,模板语法决定了生成器的表达能力
最早我做代码生成时犯过一个错误:直接用字符串替换,把 @TableName@ 换成表名,把 @fields@ 用循环拼接塞进文本。这种方式在只生成一个文件名的情况下够用,一旦模板里出现“如果字段是主键,要加 @TableId 注解,否则只加 @TableField”,字符串替换就直接失灵了
