ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

t3code实战:三段式流水线让重复CRUD代码自动生成

t3code实战:三段式流水线让重复CRUD代码自动生成 近几年接手了好几个“看起来不大、改起来要命”的业务系统之后我对“重复代码”这三个字的厌恶已经到了生理层面。订单表要写增删改查用户表要写增删改查连配置表也得写增删改查——逻辑一模一样只是字段名和表名在变。我实在不想再复制粘贴然后全局替换了于是花了两周时间做了个叫t3code的小工具。它的名字不是随便起的** t3 代表 Type → Template → Test 三段式流水线 **把“从数据模型到可用代码”的过程拆成三个独立阶段每个阶段只管一件事。这篇博文就把 t3code 从设计动机、核心实现到实际踩坑完整讲一遍。如果你也在写大量重复的脚手架代码或者想自己动手做一个代码生成器可以参考这套思路。我会把关键代码贴出来也会把那些只在真实项目里才会遇到的诡异问题交代清楚保证不是纸上谈兵。1. 为什么我会去写一个叫 t3code 的代码生成器1.1 重复代码的痛不是“数量多”而是“改不动”先说一个很典型的场景。公司内部有个老系统用的是经典的三层架构Controller、Service、Mapper。每新增一张表就要手动建实体类、VO、DTO、Mapper 接口、XML 映射文件、Service 实现、Controller 路由虽说用 IDE 的模板能省一点事但真正耗时间的是那些例外情况某个字段要加校验、某个接口要拼 SQL、某个列表要支持模糊查询。每张表多多少少和“标准模板”不一样于是你复制完还得一个个删改改到后面自己都忘了哪张表加了什么特殊逻辑。这种痛最麻烦的地方在于“改不动”。需求一变比如要给所有列表接口加一个“创建时间倒序排序”你要是不小心漏了一张表线上 Bug 就直接找你。我经历过三次这种事之后确定了一个结论靠肉眼和 Ctrl F 来管理重复逻辑迟早要出事。1.2 为什么一定要自己写而不是用现成框架其实市面上成熟的代码生成器不少像 MyBatis Generator、JHipster还有各类在线平台按说没必要重复造轮子。但我把这几个都试过一遍之后发现它们和团队现有代码风格很难完全对齐。工业级生成器默认的命名规范、目录结构、模板变量和生产代码的差异肉眼可见生成完还是要花大力气改。自己写的好处在于边界可控。我不要一个无所不能的生成平台我只要一个能把我这堆固定套路代码自动化掉的东西。于是我把需求收敛成三条输入是一份数据模型定义尽量简单地描述字段类型、注释、校验规则。输出是完整可编译运行的代码文件不需要人工二次调整。支持增量更新。表结构改了之后重新跑一次只替换应该替换的部分而不是全盘覆盖。这个收敛很重要因为一旦你试图兜住所有奇葩需求工具本身就会变成一个比项目还难维护的系统。后来我在设计 t3code 时最先定下的原则就是“只解决 80% 的常规情况剩下 20% 留给人来处理”这事才能落地。2. t3code 的核心设计三段式流水线是怎么拆出来的2.1 模型定义和三段式结构总览t3code 的输入不是数据库表结构而是一个自定义的 .t3model 文件格式接近 YAML。我把数据模型拆成三块基础信息、字段列表、补充配置。model: Order table: t_order comment: 订单表 fields: - name: id type: Long primaryKey: true comment: 主键 - name: orderNo type: String length: 32 comment: 订单编号 validation: required - name: amount type: BigDecimal precision: 10,2 comment: 订单金额 validation: range - name: status type: Integer comment: 状态1待支付 2已支付 3已取消 - name: createdAt type: LocalDateTime comment: 创建时间 options: controller: true service: true mapper: true这个文件的作用是给所有生成阶段提供唯一数据源。接下来的三段式流水线就是围绕它展开的阶段缩写输入输出职责类型解析Type.t3model 文件内部模型对象字段列表、类型映射把人类可读的描述转成程序可操作的数据模板渲染Template内部模型对象 模板文件各层代码文件的字符串按既定模板产出代码内容测试校准Test生成的代码文件校验报告或补丁文件检查生成结果是否符合语法和结构预期三个阶段相互独立因此每个阶段都能单独调试。比如你只是改了模板不需要重新解析模型你只想校验已有文件也可以单独跑 Test 阶段。2.2 为什么把“测试校准”作为独立阶段而不是生成完就结束这是我一开始没想明白后来才补上的部分。最初的版本只有 Type 和 Template 两个阶段生成完就把文件写到磁盘。结果模板一旦写错比如某个字段名拼错了就会生成一堆编译不过的代码而且错误会在各个层里重复出现。把 Test 独立出来后我做了两件事一是用 TypeScript 编译器 API 对生成的 .ts 文件做语法检查二是写了一套结构断言比如“Controller 必须包含 5 个标准方法”“Service 接口不能直接依赖 Mapper”。这样生成任何模块之后能在落盘前先发现问题不把错误带进项目。三段式设计的另一个好处是扩展容易。后来我需要支持生成前端 API 调用代码就把模板目录加了一类数据模型完全复用只新增了模板文件和对应输出规则。整个过程没有触碰 Type 和 Test 的核心逻辑改动比预想小很多。3. 核心实现拆解从模型定义到代码落盘3.1 第一阶段Type 解析器如何处理 .t3modelt3code 是 Node.js TypeScript 环境下的命令行工具。没有用花哨的 DSL直接用一个轻量的 YAML 解析库把 .t3model 读进来然后逐字段转换成内部类型系统。export interface FieldModel { name: string; type: string; targetType: string; primaryKey?: boolean; length?: number; precision?: [number, number]; comment: string; validation?: string; } export interface ModelDefinition { model: string; table: string; comment: string; fields: FieldModel[]; options: Recordstring, boolean; }这里有一个很关键的映射逻辑源类型是Long、String、BigDecimal、LocalDateTime这些贴近数据库或 Java 的类型但我要同时生成后端 Java 代码和前端 TypeScript 代码所以解析器里维护了一个双路映射表。const JAVA_TYPE_MAP: Recordstring, string { Long: Long, String: String, BigDecimal: java.math.BigDecimal, LocalDateTime: java.time.LocalDateTime, }; const TS_TYPE_MAP: Recordstring, string { Long: number, String: string, BigDecimal: number, LocalDateTime: string, };如果哪天要输出 Go 或者 Python 代码只需要在映射表里增加一个目标语言分支而不需要重新定义字段模型。这就是把类型解析单独抽出来的价值类型映射是纯函数输入固定输出固定最容易测试。3.2 第二阶段模板引擎为什么选 Handlebars模板引擎我对比过 EJS、Handlebars、Nunjucks最后选的是 Handlebars。原因很简单它语法干净不鼓励写复杂逻辑强制你只能定义 Helper。对于代码生成这种场景模板里逻辑越少越安全。我维护了一个基础模板集合示例如下。这是一个简化版的后端实体类模板package com.example.entity; import java.math.BigDecimal; import java.time.LocalDateTime; /** * {{comment}} */ public class {{model}} { {{#each fields}} /** * {{comment}} */ {{#if primaryKey}} private {{targetType}} {{name}}; {{else}} private {{targetType}} {{name}}; {{/if}} {{/each}} {{#each fields}} public {{targetType}} get{{pascalCase name}}() { return {{name}}; } public void set{{pascalCase name}}({{targetType}} {{name}}) { this.{{name}} {{name}}; } {{/each}} }真实项目中模板要复杂不少尤其是 Mapper XML 里的 insert、selectByCondition、update 这些 SQL 片段条件判断非常多。这时候 Handlebars 的#if、#unless和自定义 Helper 就派上用场了。我注册了eq相等判断、and、contains这些逻辑 Helper比如生成动态 SQL 的时候判断字段类型和校验规则{{#each fields}} {{#if (eq validation required)}} if test{{name}} ! null AND {{name}} #{ {{name}} } /if {{/if}} {{/each}}模板渲染阶段的输出就是一系列待落盘的文件内容。我把它设计成一个 mapkey 是相对路径value 是字符串内容最后统一交给 Writer 处理而不是边生成边写。这样如果你在渲染过程发现错误直接抛异常不会留下残缺文件。3.3 代码落盘覆盖策略怎么写才不会误删代码生成文件最常见的坑是“全量覆盖还是保留修改”。很多代码生成器默认全量覆盖这会造成严重问题因为生成完你总会手动加点逻辑进去下次再生成就直接被冲掉。t3code 的策略是分文件类型区别对待实体类、DTO、常量类一般不会手改落盘时全量覆盖。Service 接口、Mapper 接口几乎没有人为追加内容可以覆盖。Service 实现、Controller可能有人改或者将来一定会有人改必须合并或跳过。我最终的做法是提供一个--mode参数取值是overwrite、skip、merge。merge 模式只做一种简单的合并处理如果目标文件存在且不是本工具生成的内容就跳过如果文件头部有generated by t3code标记就完整替换。这个方案保守但安全。产出物上打标记这个习惯是从一个线上事故学来的血泪教训。3.4 命令行入口和工作流程t3code 的核心命令长这样t3code generate --model ./models/order.t3model --output ./src --mode merge它内部跑的流程可以串成一条链路读取 .t3model 文件解析为 ModelDefinition。对 ModelDefinition 做结构校验比如检查主键是否存在、字段命名是否合法。依次遍历所有模板文件渲染成内容 map。根据 mode 参数决定覆盖、跳过还是合并。最终输出“已完成 N 个文件跳过 M 个文件”的报告。生成器最容易被忽视的是未处理异常。模型里字段名写错、模板里变量拼错这种问题如果只是打印一行错误然后又继续执行生成结果会有半成品。所以我在第三步强制做全量渲染预检查任何一个模板渲染失败就直接终止不做部分输出。4. 踩过的坑模板调试、路径兼容和增量覆盖问题4.1 模板里改一个空格所有文件都变了的排查过程第一次用 t3code 给真实模块生成代码时我发现生成的代码文件全变了git diff 看过去每个文件都有一行“代码格式位置”的变动。仔仔细细对比之后才发现模板文件里写了一行带注释的空格这行空格是 IDE 格式化时帮我加上的。因为所有文件都从同一个模板渲染所以这个空格污染了所有输出。这次的教训是模板文件应该和普通源码一样严格对待。我后来在模板的工程配置里设置了保存时不自动格式化并且在渲染前后都对输出内容做了一次 normalize统一换行符为 LF去掉行尾多余空格。这种隐蔽问题不亲自踩一次根本不会想到。4.2 Windows 和 Linux 路径分隔符的差异第一次在一个 Windows 环境跑完整流程生成出来的文件路径死活不对。调试了半天问题出在path.join拼接模板输出路径时Windows 下生成了反斜杠\但代码里按正斜杠去查找文件结果就是文件写到错误位置。这个问题的修正很朴素任何涉及输出路径的拼接一律先split再join成正斜杠在生成 map 的 key 时统一使用/作为分隔符落盘时再交给操作系统去解析。写生成器的人多数在 Mac 或 Linux 上开发但工具真正跑起来的地方往往在 Windows 构建机上所以这一类兼容问题必须提前做测试。4.3 merge 模式下重复运行生成的“累积污染”merge 模式的本意是“不覆盖手工修改的代码”但这里有个陷阱。第一次运行生成了一批文件也许你什么都没改第二次运行之前你往 Controller 里加了一个自定义接口这时 merge 不会覆盖这个文件。可你第三次运行的时候这个文件里既有 t3code 生成的标准方法又有你手写的方法如果再过一个月你自己都分不清哪些是工具生成的、哪些是人写的维护起来就会很混乱。解决思路是在 merge 逻辑里增加了块注释标记。生成器把代码按方法级别打上generated注释merge 时只替换已经标记的部分插入新增的生成方法保留没有标记的手写部分。这样重复运行不会累积也不会破坏人工代码。虽然比简单 skip 复杂但实际使用体验好非常多。4.4 输出目录不是空目录时误删问题如果手滑把--output指向了项目根目录且 mode 写成 overwrite生成器理论上会把与模板同名的文件覆盖掉。幸好我在 Writer 层加了一层保护只处理 t3code 模板清单里的文件名其他文件一律不碰。这是代码生成器一个基本的安全底线——它只能改自己知道名字的文件绝不能对目录做枚举式写入。5. 用一次真实的生成过程检验 t3code 的效果5.1 从订单模型到后端代码的完整操作前面讲了不少设计思路和坑你可能会觉得有点虚。这里我把一次完整操作贴出来让效果说话。这是一个月结账单的模块模型定义开头已经给过。执行命令t3code generate --model ./models/order.t3model --output ./src --mode overwrite输出的文件清单是generated: src/main/java/com/example/entity/Order.java generated: src/main/java/com/example/dto/OrderDTO.java generated: src/main/java/com/example/vo/OrderVO.java generated: src/main/java/com/example/mapper/OrderMapper.java generated: src/main/java/com/example/mapper/OrderMapper.xml generated: src/main/java/com/example/service/OrderService.java generated: src/main/java/com/example/service/impl/OrderServiceImpl.java generated: src/main/java/com/example/controller/OrderController.java总共 8 个文件模型定义只有 70 行生成出来的代码合计有 600 多行。如果人工手写这段工作量至少要小半天用 t3code 跑一遍从输入到落盘不超过 10 秒。关键不只是快而是每次的产物都严格一致——不会因为手抖漏掉某个校验。5.2 生成后微调哪些内容必须留给人工我不建议把代码生成器做成“绝对不可改”的上帝视角工具。一次标准生成完成后以下这些内容依然需要人来确认业务状态机的流转逻辑比如订单从已支付到已取消这中间有没有退款检查工具不可能知道。复杂 SQL 的 join 条件自动生成一般只处理单表涉及多表时必须手写。权限控制注解工具能知道方法名但不知道哪些角色可以调这个接口。t3code 有点像工地上的模板脚手架它负责把结构的框架搭起来但墙里的水管怎么走还是得让水电工来定。这不是能力不足恰恰是设计时主动留出来的边界让工具不越界就不至于坏事。5.3 团队引入时的习惯调整工具做出来之后不是丢给团队一个命令行就可以。我整理了三条使用约定修改数据模型之前先跑一次生成看 diff让改动可控。所有模板统一放在仓库单独目录里模板变更要走代码评审。禁止手工修改打上generated by t3code标记的文件如果非要改先把标记移除后果自负。这三条约定看着简单但它们是把自动生成从“一次性福利”变成“长期可持续过程”的关键。没有约定用不了多久工具就会变成团队里人人畏惧的黑洞。6. 还能怎么扩展t3code 后续的玩法和边界6.1 反向解析已有数据库表结构现在的 t3code 是“模型文件驱动”也就是说你得先把 .t3model 写出来。这个门槛对老项目依然有点高。后续我计划做一个import-schema子命令直接从数据库连接信息里读取 information_schema把表结构转成 .t3model 文件。这样老模块迁移进来就不需要手工写模型定义了。这个功能实现上不复杂但要注意类型映射的兼容。比如 MySQL 的decimal(10,2)、datetime(3)PostgreSQL 的jsonb、uuid都需要映射到内部类型系统里否则生成出来的代码会有类型丢失。这一步做好工具适用面能拓宽很多。6.2 模板可视化预览与调试模式模板渲染阶段最难受的就是调试。你不知道哪个字段没渲染出来也不知道哪个 Helper 返回了空。我准备在下一版加上--preview模式生成时把渲染后的结果导入一个临时目录并生成一份“模板变量对照表”方便检查每个字段在每层模板里的值。这个对新增语言支持时的排错会很有帮助——当一份代码在新语言里报错时快速定位是类型映射的问题还是模板结构的问题会非常有价值。6.3 生成器自身也应该有测试前面说了很多生成器代码的原理和坑最后我真正想强调的一件事是生成器的产物千变万化但它自身的代码必须有自动化测试。我给 t3code 写了三种类型的测试样例单元测试覆盖解析器和模板 helper 的边界情况。黄金样例测试用固定的模型文件跑一次生成把产物作为期望值保存代码改动后比对。编译校验测试生成出真正的项目再调用编译器做全量编译。黄金样例测试对模板修改尤其敏感。如果我改了一个模板所有依赖它的文件都会变测试就会爆红提醒你重新审视这次修改是否合理。这种机制极大地防止了“改一行模板、坏一片代码”的情况。如果你也想做类似的工具我建议你从最小闭环开始拿一个真实模块把它需要生成的文件全部模板化先做到“能跑”再慢慢补增量覆盖、类型映射、结构调整这些进阶能力。别一上来就想做一个平台级的东西那会把自己绕进去。t3code 现在能稳定支撑我手头几个项目的日常开发每次新增一张表、调整一个字段跑一遍命令代码就位省下的时间用来看 diff 和写真正的业务逻辑这对我来说就是它最大的价值。
返回列表