ARTICLE DETAIL

资讯详情

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

t3code:用Go实现模板驱动代码生成器,三秒搭建CRUD业务骨架

t3code:用Go实现模板驱动代码生成器,三秒搭建CRUD业务骨架 做后端开发这几年我最烦的一件事就是从零搭一个新服务的业务骨架。明明每个模块长得都差不多控制器、服务层、仓储接口、DTO、测试桩但每次都要手写一遍复制粘贴改改类名运气不好还容易漏掉某个文件。后来我干脆花了一个周末用 Go 写了这个小工具取名t3code全称是“Type it, Template it, Transform it”——把类型定义写进去用模板展开最后转换输出成完整的业务代码骨架。它解决的核心问题就一个把“重复写业务样板代码”这件事压缩到三秒钟以内。这篇文章就聊聊这个工具的设计思路、核心参数、完整实操和我在里面踩过的坑。如果你也是被 CRUD 样板代码折磨的开发者或者想在团队里搞一套统一代码风格、减少人工 Copy Paste 出错率的脚手架工具那这篇文章应该能给你一点可复用的经验。1. 项目整体设计与思路拆解1.1 为什么不做成“重量级脚手架”一开始我也想过直接上现成的代码生成方案比如 Spring Initializr、JHipster 那套或者直接用 Maven Archetype 模板。但实际看过一圈之后我放弃了。原因有几个第一重量级脚手架绑定的技术栈太死。生成出来的工程带了一堆用不上的依赖删起来还容易删坏。团队内部往往同时有好几套技术栈在跑有写 Java 的、有写 Go 的、还有一两个遗留的 Node 服务。这时候一个绑定 Spring 生态的生成器反而成了一种束缚。第二这些工具没法沉淀团队自己的规范。每个团队的包名规范、分层方式、异常处理风格都不一样通用脚手架给的是通用结构不是你想要的结构。我要的是一个能把自己团队的代码风格写进模板里、之后每次生成都自动遵守的工具。第三也是最重要的大多数生成器只管“初始化工程”这一次。但实际开发里项目跑起来之后还要不断加新模块、新接口、新实体。你需要的是一个“随时随地可以增量生成业务代码”的工具而不是一个只负责开场的脚手架。所以 t3code 的定位从一开始就很明确一个轻量的、模板驱动的、可以增量生成业务模块的命令行工具。它不关心你用什么框架只要你提供 Java、Go、TypeScript 甚至 Python 的模板它就能帮你把对应的样板代码吐出来。1.2 t3code 的核心流水线t3code 名字里的“t3”其实是三个阶段的缩写这也是整个工具的主流程Type你把业务类型定义写进一个 JSON 或 YAML 文件里比如“我有一个 User 实体字段有 id、name、email、status需要生成 Controller、Service、Mapper 和单元测试”。Template工具读取 templates 目录下的模板文件这些文件是普通文本文件加上占位符比如{{ .EntityName }}Controller.java.tmpl这种。Transform工具把类型定义和模板合并按照配置规则处理大小写、包名替换、目录结构最终落盘生成完整的代码文件。这三步听起来简单真正写起来还是有不少细节要抠。比如模板变量怎么注入、文件名怎么从实体名自动推导、已有文件是覆盖还是跳过、生成完了要不要顺手格式化。这些细节我后面详细展开。1.3 方案选型的取舍技术选型上我直接用 Go 写命令行工具理由也很实际单个二进制文件分发团队其他人拿来就能用不需要配 Python 环境或 Node 环境标准库里的text/template足够应付模板渲染不需要引入重型模板引擎交叉编译方便Windows、Linux、macOS 都能直接用。模板语言方面我用的是 Go 自带模板语法。选它不选 FreeMarker 或者 Handlebars主要是想保持零依赖。团队里如果有人想加自定义函数直接在 Go 代码里注册进去就行扩展成本很低。提示如果你的团队完全没有 Go 背景也不用担心t3code 里暴露的模板语法非常简单基本只用得到.FieldName、{{ if }}、{{ range }}这三种结构。我后面会说明。2. 核心功能拆解与实操要点2.1 配置文件一切从模型定义开始使用 t3code 的第一步是写配置文件。默认文件名是model.yaml你可以把它放在项目根目录或者单独的specs目录下面。下面是我常用的一份配置例子project: name: user-service package: com.example.user language: java entities: - name: User table: user fields: - name: id type: Long primary: true auto: true - name: name type: String length: 64 validate: NotBlank - name: email type: String length: 128 validate: Email - name: status type: Integer default: 1 generate: controllers: true services: true mappers: true dtos: true tests: true templateDir: templates outputDir: output这份配置信息量不算大但里面有几个关键点值得说一下。entities 字段是核心。每一个实体对应一组要生成的文件。我设计的原则是所有影响代码结构的信息都放在实体定义里模板本身尽量做“傻瓜化”拿到什么就渲染什么。type 字段和语言的映射。这里的Long、String其实是我在模板里约定的“逻辑类型”不是直接的 Java 类型或 Go 类型。生成 Java 代码时模板会把Long映射成Long生成 Go 代码时则映射成int64。这个映射表我放在了一个名为type_mapping.yaml的文件里方便不同语言切换。validate 字段。这个字段很有意思它不是必须的但如果你需要生成带校验注解的 DTO就可以像示例里那样把注解全名写进去。然后 DTO 模板里就会自动打印这段校验注解。实际使用的时候你会发现这比生成完再手动补注解省了不少事。2.2 模板文件实际生成文件的“图纸”t3code 的模板就是一个普通文本文件只是文件名里带上了占位符和.tmpl后缀。举个例子我要生成一个 Controller 文件模板文件叫templates/java/{{ .EntityName }}Controller.java.tmpl内容大概长这样package {{ .Package }}; import org.springframework.web.bind.annotation.*; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; RestController RequestMapping(/{{ .EntityNameLower }}) public class {{ .EntityName }}Controller { Autowired private {{ .EntityName }}Service {{ .EntityNameLower }}Service; GetMapping(/{id}) public {{ .EntityName }}DTO getById(PathVariable Long id) { return {{ .EntityNameLower }}Service.getById(id); } GetMapping public List{{ .EntityName }}DTO list() { return {{ .EntityNameLower }}Service.list(); } PostMapping public {{ .EntityName }}DTO create(RequestBody {{ .EntityName }}DTO dto) { return {{ .EntityNameLower }}Service.create(dto); } }这里出现了四个模板变量{{ .Package }}包名直接来自配置文件的project.package。{{ .EntityName }}实体类名首字母大写比如User。{{ .EntityNameLower }}实体名转小写开头的驼峰形式比如user。这个变量由 t3code 内置函数推导模板里不用手动处理大小写。顺带一提模板里没出现字段名相关的循环因为 Controller 层用不到全部字段。如果你要生成 DTO 或者实体类就需要用{{ range .Fields }}来遍历字段了。DTO 的模板才会真正用到字段遍历package {{ .Package }}.dto; import lombok.Data; Data public class {{ .EntityName }}DTO { {{ range .Fields }} {{ if .Validate }}{{ .Validate }} {{ end }}private {{ .JavaType }} {{ .Name }}; {{ end }} }字段名对应配置里的fields数组。这样 User 实体的四个字段就会被依次渲染成 DTO 的四个属性校验注解如果有的话也会自动打上。2.3 生成器扩展不只是 Controller 和 DTO除了固定的 Controller、Service、Mapper、DTO、Test 这五类文件我实际使用中还经常往模板目录里塞一些自定义文件比如 MyBatis 的 XML 映射文件、前端 TypeScript 的 API 封装、OpenAPI 文档片段。t3code 不会限制你会生成什么——它只负责把 templates 目录下所有以.tmpl结尾的文件都渲染一遍遇到占位符就替换没有占位符就直接复制。这里就引出一个重要的设计原则约定大于配置。模板目录里的任何.tmpl文件只要目标路径合理就都会被渲染。所以如果你把模板文件放在templates/java/下面生成的时候就会按相对路径输出到output/java/目录。保持这种目录对应关系你就基本不需要额外配置“哪个模板生成到哪个目录”。3. 实操过程从零生成一个用户模块3.1 准备阶段目录结构我建议你按下面的结构组织 t3code 项目t3code/ ├── model.yaml ├── templates/ │ └── java/ │ ├── {{ .EntityName }}.java.tmpl │ ├── {{ .EntityName }}Controller.java.tmpl │ ├── {{ .EntityName }}Service.java.tmpl │ ├── {{ .EntityName }}ServiceImpl.java.tmpl │ ├── {{ .EntityName }}Mapper.java.tmpl │ ├── {{ .EntityName }}DTO.java.tmpl │ └── {{ .EntityName }}Test.java.tmpl ├── t3code.exe └── output/实际团队使用的时候我会建议把templates目录单独放一个 Git 仓库谁来更新模板、怎么更新走正常的代码评审流程。这比口头传达“大家按这个风格写代码”要靠谱得多。3.2 执行生成命令准备完毕后在项目根目录执行./t3code -config model.yaml工具会依次做以下几件事解析model.yaml文件验证entities和project字段是否完整。遍历templates/java/目录找出所有.tmpl文件。对每个实体将模板变量注入执行渲染。根据配置输出到output/java/目录。渲染完成之后自动执行一次代码格式化我用的是 Java 场景下调用google-java-format如果你不想格式化可以通过-formatfalse关掉。正常跑起来后控制台输出大概是这样的[user-service] generating entity: User - output/java/com/example/user/entity/User.java - output/java/com/example/user/controller/UserController.java - output/java/com/example/user/service/UserService.java - output/java/com/example/user/service/impl/UserServiceImpl.java - output/java/com/example/user/mapper/UserMapper.java - output/java/com/example/user/dto/UserDTO.java - output/java/com/example/user/UserTest.java done in 327ms我实测生成的 UserController 文件内容和前面模板示例基本一致只是包名、类名和字段真实填充了。整个过程从敲下命令到文件落盘确实在三秒以内。3.3 多实体批量生成实际开发里很少只加一个实体经常是一下子加五个六个。t3code 的entities字段支持数组一次配置可以批量生成多个实体。你只需要在model.yaml里继续追加entities: - name: User ... - name: Order fields: - name: id type: Long primary: true - name: userId type: Long - name: amount type: BigDecimal - name: status type: Integer执行一次命令所有实体都会生成完毕。这样新开一个订单模块的时候我就只需写好 Order 的字段定义然后跑一下 t3code整个分层代码就出来了。3.4 文件覆盖策略安全第一生成代码最怕的不是生成不出来而是把已经改过的手写代码覆盖掉。t3code 默认行为是目标文件已存在时自动跳过不覆盖。你可以在配置文件里打开强制覆盖overwrite: true但我个人强烈不建议开全局覆盖。更稳妥的办法是配合 Git 使用生成完代码之后先看一眼git diff确认没有问题之后再提交。如果实在想重新生成某个文件可以先手动删掉这个文件再执行一次生成命令。我习惯把这个流程写进团队的 Wiki 里免得有人误开覆盖然后把 Controller 里手写的业务逻辑冲掉。注意覆盖策略是“文件级别”的同一类文件要么全跳过要么全覆盖。粒度上确实不够细但换来的是实现简单和安全可控。如果你有更复杂的需求可以在模型里加force: true标记单个实体强制覆盖代码里也就七八行的事。4. 常见问题与排查技巧实录4.1 Windows 环境下模板路径不生效用 Go 写的工具在 Windows 上跑的时候最容易踩的坑是路径分隔符。模板文件名里的占位符和路径连在一起时如果直接用了硬编码的反斜杠或者正斜杠容易出现“文件找不到”的情况。我的做法是内部统一使用path/filepath处理路径但在解析模板占位符时先按正斜杠解析文件名再转换为当前系统的原生路径。这样无论模板在哪个平台编写都能正确适配。如果你在 Windows 上遇到模板文件生成了 0 字节或者直接报错“no such file”优先检查一下模板文件名里有没有手写死 Windows 反斜杠。换成{{ .EntityName }}Controller.java.tmpl这种纯占位符加正斜杠的名字问题基本就消失了。4.2 模板渲染后代码格式错乱第一次跑通生成的时候我打开输出文件一度以为自己哪里写错了——模板里缩进明明是整齐的生成的代码却乱成一团。原因很简单Go 的text/template在处理{{ range }}和{{ end }}之间的内容时会保留模板文件里原有的换行和空格。如果你的模板文件里在标签之间不小心多了一个空行渲染结果里就也会多一个空行。解决办法有两个模板文件里不要留多余空白尽量避免在指令行前后放空行。生成完成之后强制跑代码格式化。这个我前面提过Java 用google-java-formatGo 用gofmt格式化之后即使模板里有点小瑕疵也基本看不出来。以我的经验格式化是必须的不能省。哪怕模板写得很干净手工写的模板总有看走眼的时候。4.3 导入的包有冗余这个属于生成器的通病。比如说你生成一个 Controller模板里默认导入了List但实际渲染出来的代码可能并没有用到 List。Java 编译器会报“unused import”警告有些团队会直接 fail 构建。我在模板里解决这个问题的办法比较土每个模板文件的 import 部分都写成“按需最小集”。也就是说模板里不写大而全的 import 列表而是让每个字段类型自己负责引入对应的包。例如在实体模板里import javax.persistence.*; {{ range .Fields }} {{ if eq .JavaType BigDecimal }}import java.math.BigDecimal;{{ end }} {{ if eq .JavaType LocalDateTime }}import java.time.LocalDateTime;{{ end }} {{ end }}这样虽然有重复判断但换来的是输出文件始终干净无冗余。尤其是java.util.List这种高频 import如果不用就出现时间长了真的很烦。4.4 字段类型映射的坑前面提到逻辑类型和实际语言类型分离这个设计在实际维护中也有坑。最典型的是BigDecimal和LocalDateTime。这些类型在 Java 代码里如果出现在实体字段上往往还带着注解比如Column(precision 10, scale 2)。我后期给字段配置增加了一个可选属性- name: amount type: BigDecimal precision: 10 scale: 2模板里可以这样用{{ if .Precision }} Column(precision {{ .Precision }}, scale {{ .Scale }}) {{ end }} private {{ .JavaType }} {{ .Name }};这样生成的 JPA 实体就不会因为缺失精度定义导致数据库表结构不对了。这个属性是加在配置模型里的模板里直接判断使用。每次有新的特殊类型需要处理时就在模板里加一段条件判断再把需要的配置字段补上非常灵活。4.5 团队协作中的模板版本管理这个不算 bug但绝对是实际使用中绕不开的话题。最开始我把模板放在工具目录下大家各自从共享盘下载结果很快就出现“你生成的代码和我生成的代码格式不一样”的情况。后来我把 templates 目录单独抽成一个 Git 仓库并在model.yaml里增加了一个字段templateVersion: 1.2.0t3code 在执行时会先读取这个版本号和模板仓库里的版本元数据比对不一致就提示开发者先更新模板再生成。这样团队里每个人生成的代码结构都保证一致评审代码的时候就不会再有人因为这个“生成器的锅”背黑锅了。5. 延伸让 t3code 融入日常开发流程到现在为止t3code 已经在我自己的日常开发里稳定跑了大半年。除了新增模块的初始化它还被我玩出了另外几个用法这些都是一开始没有预料到的。数据库表结构同步。我会在模型定义里维护每个字段的数据库类型、长度、是否可空然后通过模板生成一份 DDL 脚本。虽然不能完全替代专业的数据库迁移工具但对于快速建新表、统一各环境的表结构来说已经够用了。接口文档片段生成。OpenAPI 注解的重复性也很高。我给模板里加了Schema、ApiOperation等注解的生成逻辑从模型定义直接输出接口文档注解省掉了不少手写文档的时间。批量生成前后端对接代码。我试着写了一个 TypeScript 的模板给定实体定义直接生成前端 API 请求函数。虽然不完美但对于内部管理系统的前后端协作来说生成的请求代码基本可以满足 80% 的场景。前端同学拿到生成结果后再手动调整边界情况比自己从零写要快得多。根据我个人的经验想用好这类生成工具最关键的一点不是把模板写得多么花哨复杂而是始终保持模型定义足够清晰、模板足够简单。模型里多一个字段所有受益的模板都能跟着增强模板里多一份复杂度每次生成带来的潜在错误风险也会同步增加。所以我的原则是模型定义宁可多写几行配置模板里绝不多写一个条件分支。最后再分享一个小技巧。在团队里推广 t3code 的时候不要急着一步到位生成所有文件先从 DTO 和实体类开始跑。这两个文件结构固定、风险最低大家用起来没有心理负担。用顺手了再逐步放开 Service、Controller 甚至测试代码的生成范围。我自己就是从只生成 DTO 开始后来慢慢把整个分层模板补齐的——你现在看到这套完整流程其实是迭代出来了七八个版本之后的结果。
返回列表