是不是每个搞开发的,都会在某个深夜冒出一个念头:这破代码我不想再写一遍了。
我在带一个后端小组的时候,这种念头几乎每周都要出现几次。业务系统越做越复杂,但代码增长的套路却高度重复:每新加一张业务表,我们就要手工写一遍 controller、service、mapper、model,再配一套接口文档、参数校验和分页查询。一个稍微完整点的模块,光代码骨架就要敲一千多行,而且大部分字符都是一模一样的模板。后来我实在受不了,花了两天时间自己搭了一个命令行小工具,代号就叫t3code。它的核心作用很简单:把“重复写代码”这件事,变成“填参数生成代码”。
这篇文章就是来聊聊 t3code 的设计思路、核心实现和实际踩坑记录。如果你也是那种被重复 CRUD 折磨的人,或者正在考虑自己做一套内部脚手架,这应该是一份足够接地气的参考。
1. 设计与思路拆解
1.1 被重复业务代码逼出来的工具
先说说初期观察。我们项目里的代码结构其实是规范的,但规范本身就意味着可预测,可预测就意味着可以用模板替代。
比如一个 user 模块,不管业务怎么变,文件结构永远是这样的:
src/main/java/com/example/controller/UserController.java src/main/java/com/example/service/UserService.java src/main/java/com/example/service/impl/UserServiceImpl.java src/main/java/com/example/mapper/UserMapper.java src/main/java/com/example/model/User.java点开这些文件,里面的内容更是高度相似。Controller 无非是@RestController加一堆@GetMapping、@PostMapping,Service 接口加几个方法签名,Mapper 里面躺着一堆 MyBatis 注解或 XML 标签。
当时我统计了一下,一个新模块大概有 35% 的代码是完全一样的,45% 是换汤不换药,只有剩下的 20% 才是真正需要人动脑子的活。与其让团队里每个成员都把这些模板文件重复敲一遍,不如写个工具把这 80% 的体力活自动化。
1.2 t3 这个代号想表达什么
很多同事问我 t3code 这个名字的来历。起名的逻辑其实很朴素:t3 代表“Top 3 + Three steps + Turbo”,也就是“三个步骤搞定核心代码”的意思。
三步指的是:
- 写配置。
- 敲一条命令。
- 拿到一套可以直接编译的代码骨架。
turbo 部分则是目标,希望从“手工写一个模块需要一下午”,压到“生成一个模块只需要几秒钟”,把省下来的时间拿去写真正有业务价值的部分。
当然,还有一个隐藏原因:好拼。命令行工具名字短一点,团队成员用起来不用翻文档。
1.3 技术路线的选择逻辑
定了目标之后,就是技术选型。这块我纠结过一阵。
一开始考虑过用 Java + Velocity,因为团队主力语言是 Java,生成出来的代码又是 Java,感觉同语言更亲切。但很快就发现问题:我们团队里不是所有人都愿意为了一个内部工具去搞一套 Maven 工程,而且 Velocity 模板语法虽然成熟,但写起来不够轻快。后来也试过用 Python 的 Jinja2,确实舒服,但跨平台分发给非 Python 背景的同事又有点麻烦。
最后选了 Node.js + Handlebars。
原因有三个:
- 安装即用:Node.js 的生态里,一个 CLI 工具打成 npm 全局包,成员一行命令就能装好。
- Handlebars 语法简单:
{{name}}这种变量替换,哪怕是没写过前端的后端同事也能看懂。 - 文件操作能力强:
fs模块处理递归创建目录、遍历文件极其方便,不用引什么重量级依赖。
这个选择让我在整个工具的开发过程中几乎没有被环境问题卡住过。
2. 核心细节解析与实操要点
2.1 配置文件的骨架设计
t3code 的使用方式是按项目走的。每个项目根目录下放一个t3config.yaml,这个文件决定了工具的“个性”。
一个典型的配置文件长这样:
project: name: mall-service packageBase: com.example.mall pathStyle: java templates: dir: .t3/templates output: root: src/main/java resourceRoot: src/main/resources presets: module: - name: controller template: controller.hbs output: "{{packagePath}}/controller/{{EntityName}}Controller.java" - name: service template: service.hbs output: "{{packagePath}}/service/{{EntityName}}Service.java" - name: serviceImpl template: serviceImpl.hbs output: "{{packagePath}}/service/impl/{{EntityName}}ServiceImpl.java" - name: mapper template: mapper.hbs output: "{{packagePath}}/mapper/{{EntityName}}Mapper.java" - name: model template: model.hbs output: "{{packagePath}}/model/{{EntityName}}.java" hooks: afterGenerate: - "cd {{projectRoot}} && npm run lint:java"重点说一下这里面的设计意图。
packageBase和pathStyle是配套的。pathStyle: java会把com.example.mall转成com/example/mall这样的目录层级,同时把模块名user转成User这样的类名。实际使用中,你可以通过pathStyle: node之类扩展名目,适配不同语言的工程结构。
presets是整个配置的灵魂。它定义了一套“模块生成预设”,每一条记录都包含:
- 使用哪个模板文件
- 生成到哪个路径
- 输出文件叫什么名字
为什么要单独拿出一节讲配置?因为我见过太多脚手架工具,把逻辑都写死在代码里,结果想改动一个命名规则都要重新发布一次。配置化的好处是你可以把工具理解成一台“模板打印机”,打印什么内容、印在哪个位置,都是由配置决定的,工具本身不关心业务。
2.2 模板引擎与变量注入规则
Handlebars 的模板语法非常直白。以下是一个简化版的 Controller 模板:
package {{packageName}}; import org.springframework.web.bind.annotation.*; import org.springframework.beans.factory.annotation.Autowired; import {{packageName}}.service.{{EntityName}}Service; @RestController @RequestMapping("/api/{{resourceName}}") public class {{EntityName}}Controller { @Autowired private {{EntityName}}Service {{entityName}}Service; @GetMapping("/{{resourceName}}/{{primaryKey}}") public {{EntityName}} getById({{primaryKeyType}} {{primaryKey}}) { return {{entityName}}Service.getById({{primaryKey}}); } @PostMapping("/{{resourceName}}") public {{EntityName}} create(@RequestBody {{EntityName}} {{entityName}}) { {{entityName}}Service.create({{entityName}}); return {{entityName}}; } }这里的{{packageName}}、{{EntityName}}、{{resourceName}}都是运行时注入的变量。
变量来源有三个渠道:
- 命令行参数,比如你输入
用户英文名和表名。 - 配置文件里写死的公共值,例如
packageBase。 - 内置函数计算的派生值,例如把
user_profile转成UserProfile。
这里要重点讲讲命名转换。因为业务里最常见的就是“输入一个 snake_case 的表名,生成一堆 CamelCase 的类名”。
t3code 内置了几个转换函数:
| 函数名 | 输入示例 | 输出示例 | 用途 |
|---|---|---|---|
pascalCase | user_profile | UserProfile | 类名 |
camelCase | user_profile | userProfile | 变量名、方法名 |
snakeCase | UserProfile | user_profile | 表名、列名 |
kebabCase | userProfile | user-profile | URL、文件名 |
在模板中调用:
public class {{pascalCase tableName}} { private {{primaryKeyType}} {{camelCase primaryKey}}; }这套设计看起来简单,但在实际使用中极其重要。很多代码生成工具做得复杂,就是因为没有在“命名统一”这一层做好抽象。一旦做好,你会发现模板表达式的可读性会大幅提升。
2.3 文件冲突与覆盖策略
代码生成工具最容易出事的地方就是文件覆盖。头一天生成好的代码,今天因为改了模板重新生成,结果把人工修改的部分直接冲掉了。
我设计 t3code 的默认策略是:不覆盖任何已存在文件。
也就是说,如果UserController.java已经存在于磁盘上,默认情况下工具会跳过这个文件,并在终端里打印一段黄色提醒,告诉你哪些文件被跳过了。你可以在命令后面加--force强制执行覆盖,也可以加--diff先把差异打印出来看看再决定。
这里有一个我后来加进去的功能:生成前自动备份。执行带覆盖的生成时,工具会把目标文件复制到一个.t3/backup目录并带上时间戳。虽然听着土,但已经救过我至少两次,尤其是模板改到一半发现逻辑写错的时候。
3. 实操过程与核心环节实现
3.1 初始化项目与全局命令安装
t3code 发布为一个 npm CLI 工具。安装过程很寻常:
npm install -g t3code然后进入你的项目目录,执行:
t3code init这个命令会做三件事:
- 在项目根目录生成一个
t3config.yaml配置模板。 - 在项目根目录创建一个
.t3隐藏目录,里面放默认模板。 - 如果检测到项目是 Maven 结构,会自动识别
src/main/java等路径。
这里有个小细节值得留意:.t3目录在 Git 仓库里要不要提交?
我的建议是提交。虽然有人认为工具配置是个人偏好,但站在团队角度,模板和配置的一致性是代码风格统一的前提。新人拉下仓库后,只需要安装全局命令,再执行t3code make module就能生成和其他成员一模一样的代码结构。这种“团队级的一致性”带来的协作收益,远超那一点模板文件的维护成本。
3.2 从零生成一个完整业务模块
假设现在要新增一个“收货地址”模块,表名叫shipping_address。
一条命令搞定:
t3code make module shipping_address --primary-key id --primary-key-type Long这条命令执行后,工具的处理流程大致如下。
首先,解析参数,生成一组上下文变量:
tableName: shipping_address entityName: shippingAddress EntityName: ShippingAddress resourceName: shipping-address packageName: com.example.mall.model packagePath: com/example/mall/model primaryKey: id primaryKeyType: Long然后,遍历presets中的五条记录,对每条记录执行三步:
- 定位模板文件
- 渲染模板内容
- 写入目标文件
核心实现(伪代码)如下:
function makeModule(name, options) { const ctx = buildContext(name, options); const presets = config.presets.module; for (const item of presets) { const content = renderTemplate(item.template, ctx); const targetPath = resolveOutput(item.output, ctx); writeFileSmart(targetPath, content, options); } runHooks('afterGenerate', ctx); }这段逻辑看起来简单,但里面有几个容易被忽略的细节。
第一个细节是buildContext的健壮性。用户可能输入ShippingAddress、shippingAddress、shipping_address,我需要工具能够标准化地识别用户意图。我的做法是:先用一个小函数做输入清理,把非字母数字的字符统一替换为空格,再按空格或驼峰拆分单词,最后重新拼接成四个命名风格。
第二个细节是resolveOutput时路径分隔符的跨平台问题。一开始我在 Windows 上跑测试时遇到了路径错误,后来统一用path.join来处理,问题就消失了。这个坑在后面常见问题里会再提。
3.3 生成结果演示
执行完命令后,项目里会多出这样一组文件:
src/main/java/com/example/mall/controller/ShippingAddressController.java src/main/java/com/example/mall/service/ShippingAddressService.java src/main/java/com/example/mall/service/impl/ShippingAddressServiceImpl.java src/main/java/com/example/mall/mapper/ShippingAddressMapper.java src/main/java/com/example/mall/model/ShippingAddress.java以生成的 Model 为例:
package com.example.mall.model; import lombok.Data; @Data public class ShippingAddress { private Long id; private String receiverName; private String receiverPhone; private String province; private String city; private String detailAddress; }字段列表哪来的?这就要说到第二个命令了。
3.4 从数据库表结构直接生成字段
手写模板只能处理公共结构,字段还是要一个个填。我后来又加了sync-table子命令:
t3code sync-table shipping_address --table-schema mall这个命令会连接数据库,读取目标表的列信息,然后自动更新 Model 和 Mapper XML。它内部做的工作是:
- 查
information_schema.columns - 把
receiver_name转成receiverName - 根据
data_type映射 Java 类型 - 生成字段列表并注入上下文
数据库连接配置放在t3config.yaml里,只用于开发环境。这里有一个安全提示:千万不要在公共配置里写生产库的地址,这种代码生成工具只应该连接开发或测试库,不然一个手滑,可能线上数据就危险了。
4. 常见问题与排查技巧实录
4.1 高频报错与对应处理
任何一个工具,用的人多了,遇到的问题就千奇百怪。t3code 上线后,我收集到的高频问题主要集中在以下几个方面,整理成速查表:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
生成的类名不对,比如UserProfile变成了Userprofile | 输入命名不规范或转换函数使用错误 | 确认输入使用snake_case表名,模板内统一用pascalCase |
模板中{{#if}}判断不生效 | 上下文变量名拼写错误 | 先执行t3code debug查看完整变量列表 |
Windows 上生成后文件路径带了反斜杠\ | 代码里硬编码了/分隔符 | 升级到使用path.join的版本 |
| 生成文件是 UTF-8 但编译乱码 | Windows 记事本保存时带了 BOM | 在模板文件头部不允许 BOM,t3code 读取时自动剥离 |
| 重复执行后文件被跳过 | 默认覆盖策略生效 | 用--diff查看差异,再用--force覆盖 |
模块名是class等 Java 关键字 | 用户没做检查 | t3code 内置关键字黑名单,此时自动拼接前缀Model |
这里重点提一下调试技巧。我发现不少人用代码生成工具遇到问题时,第一反应是百度,但大多数时候只需要一个命令就能定位问题:t3code debug。它会打印出当前上下文里所有的变量和值,你一眼就能看出是EntityName拼错了还是tableName解析错了。这个命令我后来建议团队里每个人都先跑一遍再报 bug,反馈效率直接翻倍。
还有一个常见问题来自集成开发环境的缓存。改了模板重新生成代码后,IDE 偶尔会提示“文件被外部修改”,要求手动同步。通常重启 IDE 或执行mvn compile就能激活全量刷新。这一点在 IntelliJ IDEA 里尤其明显,我后来在工具帮助文档里特意加了一条说明。
4.2 值得长期养成的使用习惯
工具做好只是第一步,让团队养成正确的使用姿势是更重要的一步。我这里分享几个自己用出来的习惯。
第一个习惯是:把生成操作纳入代码提交的必经流程。比如我们约定新增业务表后,必须跑一次t3code sync-table并把生成的代码一并提交。这样做的结果是,代码评审时不会再出现“这个 Model 缺少字段”这类低级问题,因为字段是从数据库同步过来的,和表结构天然一致。
第二个习惯是:模板要纳入 Git 管理并定期 review。模板是工具的“灵魂”,如果模板里写死了过时的包名或过时的注解风格,生成出来的代码也会带着臭味。我们团队每季度会做一次模板 review,把新发现的最佳实践沉淀进去。
第三个习惯是:用 dry-run 模式规范操作。我在工具里加入了--dry-run参数,执行时只打印“将要写哪些文件、内容长什么样”,但不真正写入磁盘。这个参数在模板大改时格外有用,可以在不污染代码仓库的情况下提前预览效果。
4.3 代码生成后的二次修改底线
必须要说的是,代码生成工具的目标是帮你完成枯燥的骨架搭建,但不是让你完全丢掉思考。
我们团队有一条不成文的约定:生成的代码允许二次修改,但修改后要把差异点反馈到模板里。
举个例子,有一次生成出来的分页查询条件漏掉了一个deleted逻辑删除判断,同事在生成的代码里手动补上了。按规矩,他需要同步修改 Mapper 模板,让后续所有生成的模块都带上这个逻辑。如果只改生成结果而不同步模板,下次重新生成时问题还会复现。这个机制保证了工具在团队里是持续进化的,而不是生成一次就死在角落。
5. 后续可以怎么扩展
t3code 做到现在这个程度,已经能覆盖我们团队 80% 的重复编码场景。不过它的扩展空间还很大,我简单列几个方向供参考。
一个是支持多语言模板包。目前内置的模板主要面向 Java + Spring Boot,但只要你自己在.t3/templates里新增模板并配置新的 preset,完全可以生成 Python FastAPI、Go Gin 甚至前端页面代码。它本质上是一个“模板渲染引擎 + 文件搬运器”,语言无关。
另一个是结合自动化测试。我的设想是:每改一次模板,自动生成一个模块样例,然后跑一遍编译和单测,确保模板没有引入语法错误。这个现在还在规划中,但思路是成熟的,等于给代码生成工具也加上 CI。
还有一个方向是埋点统计,比如记录每个团队成员的生成频率、常用模块类型。一开始觉得这是噱头,但后来发现统计结果能反哺模板优化,比如某个模块生成频率陡然升高,说明业务正在向某个方向集中,值得提前准备更细化的模板。
从我个人实际操作的角度讲,做 t3code 最大的收获不是代码量减少了多少,而是让团队开始认真思考“哪些代码是真正需要人写的,哪些只是机械重复”。这个思考过程本身,往往比工具本身更有价值。如果你也想搞一套自己的代码生成器,建议从小处起步:先挑一个你每周都要手写两三次的模块,把模板做出来,跑通一条生成流程,剩下的自然就会慢慢长出来。