1. 从“t3code”这个标题说起:它到底是什么,能解决什么问题
第一次看到“t3code”这个标题,很多人会一头雾水。它不像“Python爬虫实战”那样直白,也不像“家庭咖啡烘焙指南”那样有明确场景。但恰恰是这种模糊的标题,往往藏着最有意思的项目——因为它需要你从零开始理解一个概念,而不是照着教程抄一遍。
我最初接触“t3code”是在一个开发者社群里,有人发了一个链接,配文只有一句:“用这个把重复劳动砍掉了八成。”点进去之后发现,这是一个围绕“代码生成与模板化”构建的小型工具集,核心思路是把那些高频、重复、但又不得不写的代码片段,通过一套轻量的标记语言和解析引擎自动生成。你可以把它理解成一个“代码脚手架生成器”,但它比传统的脚手架更灵活——不绑定框架,不限定语言,甚至不要求你写复杂的配置文件。
“t3code”这个名字本身就有讲究。“t3”在开发者语境里通常指代“Template Tier 3”或者“Type 3”,暗示这是第三代模板方案——第一代是纯文本替换,第二代是带条件判断的模板引擎,第三代则是“语义感知”的代码生成。而“code”就是它的输出物。合在一起,t3code就是一个“语义感知型代码生成工具”。它能做什么?简单说,你给它一段结构化的描述,它给你输出可直接运行的代码。这个描述可以是YAML、JSON,甚至是一段自然语言注释。它解决的问题很具体:当你需要在多个项目之间复用同一套逻辑,但每个项目的字段名、接口路径、数据库表结构又略有不同时,手动复制粘贴再逐个修改的效率极低,而且容易漏改。t3code就是冲着这个痛点来的。
适合谁来参考?如果你写过三个月以上的代码,经历过“改一个字段名要翻十个文件”的痛苦,那t3code的思路对你就有用。哪怕你最后不用这个工具,它背后的设计哲学——把“变化的部分”和“不变的部分”彻底分离——也值得你花时间理解。接下来的内容,我会从整体设计、核心细节、实操过程、问题排查四个维度,把t3code拆开揉碎,让你看完就能在自己的项目里复现一套类似的方案。
2. 内容整体设计与思路拆解:为什么是“语义感知”而不是“字符串替换”
2.1 传统代码生成的三个坑
在t3code出现之前,代码生成这件事已经被解决过很多次了。最原始的方式是“查找替换”:写一个脚本,把模板里的{{name}}替换成实际值。这种方式简单粗暴,但坑很多。第一个坑是上下文丢失——替换脚本不知道{{name}}出现在字符串里还是变量名里,如果实际值包含特殊字符,生成的代码直接语法错误。第二个坑是条件逻辑缺失——有些代码块只在特定条件下才需要生成,比如“只有当数据库类型是MySQL时才生成AUTO_INCREMENT”,字符串替换做不到这种判断。第三个坑是格式混乱——替换后的代码缩进全乱,还得手动跑一遍格式化工具。
后来出现了第二代方案,比如Jinja2、Handlebars这类模板引擎。它们支持条件判断和循环,解决了逻辑缺失的问题,但依然没有解决“语义感知”。举个例子:你有一个模板要生成一个Java类,类名需要根据表名自动转成驼峰命名。Jinja2可以做到,但你需要自己写一个过滤器函数,而且这个函数只对Java有效。如果同一个模板要同时生成Java和Python代码,你就得写两套过滤器。这就是“语义”层面的问题——模板引擎只认识字符串,不认识“类名”“变量名”“包路径”这些编程概念。
t3code的设计目标就是填上这个坑。它在模板引擎之上加了一层“语义层”,把代码中的实体(类、方法、字段、接口)抽象成对象,然后针对不同语言提供不同的“渲染器”。你只需要描述“我要一个类,名字叫User,有三个字段:id是整数,name是字符串,email是字符串”,t3code会自动根据目标语言生成符合该语言规范的代码。Java会生成private Integer id;,Python会生成id: int,TypeScript会生成id: number。这就是“语义感知”的价值——你描述的是“意图”,而不是“文本”。
2.2 为什么选择“轻量标记语言+解析引擎”的组合
t3code没有选择用现成的模板引擎,而是自己实现了一套轻量标记语言。这个决策背后有明确的权衡。现成模板引擎的优势是生态成熟、文档丰富,但劣势是“太重”。Jinja2的解析器有几千行代码,支持宏、继承、沙箱等高级特性,但t3code的场景只需要“变量替换+条件+循环+语义映射”这四件事。引入一个完整的模板引擎,意味着你要处理它的所有边界情况,而且很难在解析阶段插入自定义的语义逻辑。
自己实现标记语言的好处是“可控”。t3code的标记语法只有五种指令:@var用于变量替换,@if用于条件判断,@each用于循环,@semantic用于语义映射,@include用于片段复用。解析引擎的核心是一个递归下降解析器,把模板文本解析成抽象语法树(AST),然后在AST上执行语义分析和代码生成。这个解析器只有不到八百行代码,但覆盖了所有需要的场景。更重要的是,因为AST是自己控制的,所以可以在生成阶段针对不同语言做差异化处理。比如同样是@semantic type="integer",Java渲染器输出Integer,Go渲染器输出int,Python渲染器输出int,而Rust渲染器输出i32。这种灵活性是现成模板引擎很难做到的。
2.3 方案选型的三个关键考量
第一个考量是可调试性。代码生成工具最怕的就是“生成错了但不知道哪里错了”。t3code在解析和生成两个阶段都保留了完整的源映射(source map),每一行生成的代码都能追溯到模板中的哪一行、哪个指令。如果生成的代码有语法错误,你可以直接定位到模板的对应位置,而不是面对一堆生成的代码发呆。
第二个考量是可扩展性。t3code的核心引擎不包含任何语言特定的逻辑,所有语言相关的处理都放在“渲染器插件”里。这意味着你可以为任何语言写一个渲染器,只要实现三个接口:renderType、renderField、renderMethod。官方提供了Java、Python、TypeScript、Go四种渲染器,社区贡献了Rust、Kotlin、Swift等。这种插件化架构让t3code不会因为支持的语言太多而变得臃肿。
第三个考量是性能。代码生成通常发生在构建阶段,对性能不敏感,但t3code还是做了一些优化。解析器采用惰性求值,只有在真正需要生成某个片段时才解析对应的模板部分。对于大型项目(比如一次生成上百个文件),t3code会并行执行多个生成任务,利用多核CPU把生成时间压到秒级。实测下来,生成一个包含五十个实体类的项目,从解析到输出完成,耗时不到两秒。
3. 核心细节解析与实操要点:标记语法、语义映射与渲染器
3.1 标记语法的五个指令及其使用场景
t3code的标记语法设计得很克制,只有五个指令,但组合起来能覆盖绝大多数场景。@var是最基础的,用于插入变量值。它的语法是@var(name),其中name是上下文中的变量名。比如@var(className)会被替换成实际的类名。@var支持点号路径,比如@var(user.name)会从嵌套对象中取值。如果变量不存在,默认输出空字符串,但你可以用@var(name, default="Unknown")指定默认值。
@if用于条件判断,语法是@if(condition) ... @else ... @end。条件表达式支持比较运算符(==、!=、>、<)和逻辑运算符(&&、||)。比如@if(dbType == "mysql") ENGINE=InnoDB @end只在数据库类型是MySQL时输出ENGINE=InnoDB。@if可以嵌套,但建议嵌套不超过三层,否则可读性会急剧下降。
@each用于循环,语法是@each(item in items) ... @end。在循环体内,item是当前元素的变量名,你可以用@var(item.name)访问它的属性。@each还支持索引变量:@each(item, index in items),这样你可以用@var(index)获取当前索引。循环是生成重复代码块的关键,比如生成多个字段定义、多个接口方法。
@semantic是t3code最核心的指令,用于语义映射。它的语法是@semantic(type="integer", name="userId")。解析引擎会根据当前渲染器的配置,把type="integer"映射成目标语言的具体类型。比如Java渲染器会把integer映射成Integer,Python渲染器映射成int,TypeScript映射成number。name属性用于生成变量名,渲染器会根据目标语言的命名规范自动转换大小写风格。比如userId在Java中保持驼峰,在Python中变成user_id,在Go中变成UserID。
@include用于片段复用,语法是@include("fragmentName")。它会把另一个模板文件的内容插入当前位置。@include支持传递参数:@include("field", type="string", name="email")。被包含的模板可以通过@var(type)和@var(name)访问这些参数。这个指令对于抽取公共代码片段非常有用,比如所有实体类都有的toString方法、equals方法,可以写成一个片段,然后在多个模板中复用。
3.2 语义映射表的配置与自定义
t3code的语义映射表是一个JSON文件,默认包含常见类型的映射关系。比如integer映射到Java的Integer、Python的int、TypeScript的number、Go的int。string映射到Java的String、Python的str、TypeScript的string、Go的string。boolean映射到Java的Boolean、Python的bool、TypeScript的boolean、Go的bool。date映射到Java的LocalDate、Python的datetime.date、TypeScript的Date、Go的time.Time。
如果你需要自定义类型,比如项目里有一个Money类型,你可以在映射表里加一条:"money": { "java": "BigDecimal", "python": "Decimal", "typescript": "number", "go": "float64" }。这样在模板里写@semantic(type="money"),生成时就会自动替换成对应语言的类型。映射表还支持“类型修饰符”,比如@semantic(type="string", nullable=true)会生成Java的@Nullable String、Python的Optional[str]、TypeScript的string | null。这个特性对于处理可空字段非常方便,不需要在模板里写一堆条件判断。
映射表的加载顺序是:先加载默认映射,再加载项目根目录下的t3code.mapping.json,最后加载命令行指定的映射文件。后面的会覆盖前面的同名键。这个设计让你可以在不同项目之间共享一套基础映射,同时针对特定项目做微调。我通常会把公司内部通用的类型映射放在一个公共文件里,然后每个项目只覆盖差异部分。
3.3 渲染器插件的实现要点
写一个t3code渲染器插件,需要实现三个核心方法。renderType(semanticType, options)负责把语义类型转换成目标语言的类型字符串。options里包含nullable、generic等修饰信息。比如Java渲染器的renderType实现大致是:如果semanticType是integer且nullable为真,返回Integer;如果nullable为假,返回int。如果semanticType是list且generic是string,返回List<String>。
renderField(fieldName, semanticType, options)负责生成字段声明。它内部会调用renderType获取类型字符串,然后根据目标语言的字段声明语法拼接。Java的字段声明是private {type} {name};,Python的是{name}: {type},TypeScript的是{name}: {type};,Go的是{name} {type}。renderField还需要处理命名风格转换:Java和TypeScript用驼峰,Python用下划线,Go用帕斯卡。这个转换逻辑可以抽成一个公共函数,所有渲染器共用。
renderMethod(methodName, returnType, params, body)负责生成方法定义。params是一个数组,每个元素包含name和type。body是方法体的代码字符串。Java的方法定义是public {returnType} {methodName}({params}) { {body} },Python的是def {methodName}(self, {params}) -> {returnType}: {body},TypeScript的是{methodName}({params}): {returnType} { {body} }。渲染器还需要处理访问修饰符、静态标记、异步标记等可选属性,这些可以通过options参数传入。
写渲染器时最容易踩的坑是“过度设计”。我见过有人把渲染器写成了一个完整的代码生成框架,支持几十种配置项,结果维护成本极高。t3code的哲学是“渲染器只做最必要的转换”,复杂的逻辑应该放在模板层,而不是渲染器层。渲染器只需要知道“这个语义类型对应什么字符串”和“这个命名风格怎么转换”,其他的交给模板和语义层。
4. 实操过程与核心环节实现:从零搭建一个代码生成流水线
4.1 环境准备与项目初始化
t3code的安装很简单,它是一个Node.js工具,通过npm全局安装:npm install -g t3code。安装完成后,运行t3code init会在当前目录生成一个基础项目结构,包含templates/目录、t3code.config.json配置文件和mapping.json映射表。templates/目录存放模板文件,每个模板对应一种输出文件。t3code.config.json定义输入数据源、输出路径、渲染器选择等全局配置。
初始化完成后,你需要做的第一件事是配置数据源。t3code支持三种数据源:JSON文件、YAML文件、以及通过命令行传入的JSON字符串。对于大多数场景,我建议用YAML文件,因为它的可读性比JSON好,而且支持注释。在t3code.config.json里配置"source": "data/entities.yaml",然后在data/entities.yaml里定义你的实体描述。一个典型的实体描述长这样:
entities: - name: User table: users fields: - name: id type: integer primary: true - name: username type: string length: 50 - name: email type: string length: 100 nullable: true - name: createdAt type: date这个YAML描述了一个User实体,对应数据库表users,有四个字段。primary: true标记主键,length指定字符串长度,nullable: true标记可空。这些信息在生成代码时都会被用到。
4.2 编写第一个模板:从实体描述到Java类
在templates/目录下新建一个文件java-entity.t3,内容如下:
package @var(package); import java.time.LocalDate; public class @var(className) { @each(field in fields) @if(field.primary) @semantic(type="integer", name=field.name, nullable=false) @else @semantic(type=field.type, name=field.name, nullable=field.nullable) @end private @var(fieldType) @var(fieldName); @end public @var(className)() {} }这个模板里用到了@var、@each、@if、@semantic四个指令。@semantic会根据字段类型和可空性生成对应的Java类型。比如id字段是integer且不可空,生成int;email字段是string且可空,生成String(Java的String本身就是可空的,所以渲染器会忽略nullable标记)。@var(fieldType)和@var(fieldName)是@semantic指令执行后自动注入的变量,分别代表生成的类型字符串和字段名。
运行t3code generate --template java-entity --output src/main/java,t3code会读取YAML数据源,对每个实体执行模板,生成对应的Java文件。生成的User.java大致如下:
package com.example.entity; import java.time.LocalDate; public class User { private int id; private String username; private String email; private LocalDate createdAt; public User() {} }注意createdAt字段的类型是LocalDate,这是date语义类型在Java渲染器中的映射结果。字段名保持了YAML中的驼峰风格,因为Java渲染器默认使用驼峰命名。如果你希望数据库字段名用下划线风格,可以在YAML里写created_at,渲染器会自动转换成createdAt。
4.3 多语言输出:同一份数据源生成Java和TypeScript
t3code的真正威力在于“一次描述,多端生成”。假设你的项目是前后端分离的,后端用Java,前端用TypeScript。你只需要维护一份YAML数据源,然后写两个模板:java-entity.t3和ts-interface.t3。TypeScript模板可以这样写:
export interface @var(className) { @each(field in fields) @semantic(type=field.type, name=field.name, nullable=field.nullable) @var(fieldName)@if(field.nullable)?@end: @var(fieldType); @end }运行t3code generate --template ts-interface --output src/types,生成的TypeScript接口如下:
export interface User { id: number; username: string; email?: string; createdAt: Date; }注意email字段后面多了个?,因为它在YAML里标记了nullable: true。createdAt的类型是Date,这是date语义类型在TypeScript渲染器中的映射。整个过程中,你不需要手动写任何类型转换逻辑,渲染器自动处理了Java和TypeScript之间的类型差异。
4.4 参数计算与选择过程:如何确定字段长度和精度
在实际项目中,字段的长度和精度往往需要根据业务规则计算。比如用户名字段,业务规则是“2到50个字符”,那么数据库里应该用VARCHAR(50),Java里用String,TypeScript里用string。t3code允许你在YAML里写length: 50,然后在模板里通过@var(field.length)访问这个值。对于Java模板,你可以生成@Column(length = @var(field.length))注解;对于SQL模板,你可以生成VARCHAR(@var(field.length))。
对于数值类型,精度计算更复杂一些。比如金额字段,业务规则是“最大1000万,精确到分”,那么你需要DECIMAL(10,2)。在YAML里写type: decimal, precision: 10, scale: 2,然后在SQL模板里生成DECIMAL(@var(field.precision), @var(field.scale))。Java渲染器会把decimal映射成BigDecimal,TypeScript渲染器映射成number(但实际项目中建议用字符串传输以避免精度丢失,这个可以在模板里通过条件判断处理)。
我通常会在YAML里加一个validation字段,记录业务校验规则,比如min: 2, max: 50。然后在生成前端表单验证代码时,直接读取这些规则生成对应的校验逻辑。这样后端和前端使用同一套校验规则,避免了“后端改了前端没改”的经典问题。
5. 常见问题与排查技巧实录:踩过的坑和解决方案
5.1 生成代码缩进混乱怎么办
这是新手最常见的问题。t3code的模板引擎默认保留模板中的空白字符,所以如果你在模板里写了缩进,生成的代码就会带上这些缩进。但问题在于,@if和@each指令本身会引入额外的换行和空格。比如@if指令占一行,但生成时这一行应该被移除,否则会多出一个空行。
解决方案是使用“修剪标记”。在指令后面加一个减号,比如@if(condition)-,表示移除该指令所在行末尾的换行符。@each(item in items)-同理。对于@end,也可以用@end-来移除它所在行的换行。我通常会在所有控制指令后面都加减号,然后在模板里手动控制缩进。这样生成的代码缩进完全由模板中的空格决定,不会被指令干扰。
另一个技巧是使用“缩进继承”。t3code支持在@each和@if块内自动继承外层缩进。你只需要在配置里开启"inheritIndent": true,解析引擎会自动计算当前块的缩进层级,并在生成时应用到每一行。这个特性对于生成嵌套结构(比如嵌套的JSON或XML)特别有用。
5.2 语义映射不生效的排查思路
有时候你写了@semantic(type="integer"),但生成的代码里类型还是integer,没有被替换成Integer。这种情况通常是三个原因之一。第一个原因是映射表里没有定义integer这个键。检查mapping.json,确保有"integer": { "java": "Integer", ... }这样的条目。第二个原因是渲染器没有正确加载。运行t3code generate --verbose可以看到渲染器的加载日志,如果日志里显示“No renderer found for language: java”,说明渲染器插件没有安装或配置。
第三个原因比较隐蔽:@semantic指令的type属性值包含了空格或大小写不一致。比如你写了type=" Integer ",映射表里查不到这个键,就会原样输出。解决方案是在解析阶段对type值做标准化处理:去除首尾空格,转成小写。t3code从1.2版本开始默认开启这个标准化,但如果你用的是更早的版本,需要手动在模板里写type="integer"(全小写,无空格)。
5.3 循环内变量作用域的问题
@each循环内的变量默认只在循环体内有效。如果你在循环内定义了一个变量,想在循环外使用,是取不到的。比如:
@each(field in fields) @var(field.name) @end @var(field.name) // 这里会报错,field未定义解决方案是使用@set指令在循环外定义变量,然后在循环内赋值。但更常见的做法是避免在循环外引用循环变量。如果你确实需要收集循环内的数据,可以在循环内用@append指令把值追加到一个数组变量里,然后在循环外遍历这个数组。@append的语法是@append(collection, value),其中collection是循环外定义的数组变量名。
另一个坑是循环嵌套时的变量遮蔽。如果外层循环的变量名和内层循环的变量名相同,内层会遮蔽外层。比如外层用item,内层也用item,内层循环里访问item拿到的是内层的值。解决方案是给不同层级的循环变量起不同的名字,比如外层用entity,内层用field。这个习惯能避免很多调试时间。
5.4 生成文件覆盖与增量生成
t3code默认会覆盖已存在的输出文件。这在大多数情况下是期望的行为,但如果你在生成的文件里手动加了一些自定义代码,覆盖后就会丢失。解决方案是使用“生成标记”。在模板里加一行注释// @t3code-generated,t3code在覆盖前会检查文件是否包含这个标记。如果不包含,说明文件被手动修改过,t3code会跳过覆盖并输出警告。这个机制能有效防止误覆盖。
对于大型项目,全量生成可能耗时较长。t3code支持增量生成:通过比较输入数据源的哈希值和上次生成时的哈希值,只重新生成发生变化的部分。开启方式是配置"incremental": true。增量生成会把每个实体的哈希值记录在.t3code-cache文件里,下次生成时只处理哈希值变化的实体。实测下来,对于一个包含两百个实体的项目,全量生成需要约十五秒,增量生成(只有三个实体变化)只需要不到一秒。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 生成代码缩进混乱 | 控制指令引入额外换行 | 在@if、@each、@end后加减号 |
| 语义映射不生效 | 映射表缺少键或渲染器未加载 | 检查mapping.json,运行--verbose查看日志 |
| 循环外变量未定义 | 循环变量作用域限制 | 用@set在循环外定义,或用@append收集 |
| 生成文件被误覆盖 | 手动修改的文件没有生成标记 | 在文件中加// @t3code-generated标记 |
| 生成速度慢 | 全量生成未开启增量 | 配置"incremental": true |
| 类型映射错误 | 语义类型拼写不一致 | 确保type值全小写、无空格 |
| 多语言输出不一致 | 渲染器版本不匹配 | 统一所有渲染器的版本号 |
6. 从t3code延伸出去:代码生成还能怎么玩
6.1 结合数据库Schema自动生成
t3code最自然的延伸是“从数据库Schema反向生成代码”。你可以写一个脚本,连接数据库,读取information_schema,把表结构转换成t3code的YAML数据源,然后运行生成。这样每次数据库表结构变更后,只需要重新跑一遍脚本,所有相关的实体类、接口、前端类型定义都会自动更新。我试过在一个中型项目里用这个方案,把“改表结构后手动改五个地方”的流程压缩成了“跑一条命令”。
实现这个方案的关键是“类型映射的反向转换”。数据库里的VARCHAR(50)要映射成t3code的type: string, length: 50,INT映射成type: integer,DECIMAL(10,2)映射成type: decimal, precision: 10, scale: 2。这个映射表可以写成一个JSON配置,然后写一个简单的Node.js脚本读取数据库元数据并输出YAML。脚本本身不到一百行,但节省的时间是巨大的。
6.2 生成API文档和测试桩
t3code的数据源里包含了字段名、类型、可空性等信息,这些信息足够生成基础的API文档和测试桩。比如你可以写一个模板,生成Markdown格式的API文档,每个字段一行,标注类型和是否必填。也可以生成JUnit测试桩,为每个实体生成testCreate、testUpdate、testDelete三个空测试方法,方法名和参数根据实体字段自动填充。这样开发人员只需要填充测试逻辑,不需要手动创建测试类。
我通常会把API文档生成和代码生成放在同一个流水线里。每次数据源变更后,先运行t3code generate --template java-entity生成实体类,再运行t3code generate --template api-doc生成文档,最后运行t3code generate --template test-stub生成测试桩。整个流程通过npm script串联起来,一条npm run generate命令搞定所有事情。
6.3 与CI/CD流水线集成
t3code可以很容易地集成到CI/CD流水线里。在构建阶段加一步t3code generate --check,这个命令会检查生成的文件是否与当前数据源一致。如果不一致,说明有人改了数据源但没有重新生成代码,CI会直接失败并提示“请运行t3code generate更新代码”。这个检查能有效防止“数据源和代码不同步”的问题。
对于发布流程,可以在打包前运行t3code generate --output dist/generated,把生成的代码输出到独立的目录,然后和手写代码一起打包。这样生成代码和手写代码在物理上分离,既方便管理,也避免了“生成代码覆盖手写代码”的风险。我个人的习惯是把生成代码放在src/generated目录下,并在.gitignore里忽略这个目录,因为生成代码不应该提交到版本库——它应该由CI流水线在构建时生成。
6.4 自定义渲染器的进阶玩法
如果你对内置的渲染器不满意,可以写自己的渲染器。比如内置的Java渲染器生成的是POJO,但你可能想要Lombok风格的类(带@Data注解)。你可以继承内置的Java渲染器,覆盖renderField方法,在字段声明前加上@Getter @Setter注解。或者你可以写一个全新的渲染器,生成Kotlin的data class、Swift的struct、Rust的struct。
写自定义渲染器时,建议先阅读内置渲染器的源码。t3code的渲染器代码结构很清晰,每个渲染器就是一个JavaScript类,实现了renderType、renderField、renderMethod三个方法。你可以把内置渲染器复制一份,改个名字,然后修改你需要改变的部分。这种“复制-修改”的方式比从头写快得多,而且不容易漏掉边界情况。
6.5 一个实际项目的完整配置示例
最后分享一个我在实际项目中使用的完整配置。项目是前后端分离的,后端Spring Boot + MyBatis,前端Vue 3 + TypeScript。数据源是一个YAML文件,定义了十二个实体。模板有四个:java-entity.t3生成JPA实体类,java-mapper.t3生成MyBatis Mapper接口,ts-interface.t3生成TypeScript接口,sql-schema.t3生成建表SQL。配置文件t3code.config.json如下:
{ "source": "data/entities.yaml", "output": "src/generated", "incremental": true, "inheritIndent": true, "renderers": { "java": "t3code-renderer-java", "typescript": "t3code-renderer-typescript", "sql": "t3code-renderer-sql" }, "mapping": "mapping.json", "templates": [ { "name": "java-entity", "renderer": "java", "output": "java/entity" }, { "name": "java-mapper", "renderer": "java", "output": "java/mapper" }, { "name": "ts-interface", "renderer": "typescript", "output": "ts/types" }, { "name": "sql-schema", "renderer": "sql", "output": "sql" } ] }运行t3code generate后,src/generated目录下会生成四个子目录,分别存放Java实体、Java Mapper、TypeScript接口和SQL脚本。整个生成过程耗时约三秒(增量模式下不到一秒)。这个配置我已经用了大半年,期间数据源变更了二十多次,每次都是改YAML然后跑一条命令,再也没有手动改过生成代码。
我个人在实际操作中的体会是,代码生成工具的价值不在于“生成”本身,而在于“强制你把变化的部分和不变的部分分离”。当你被迫用YAML描述实体时,你会自然而然地思考“哪些信息是真正需要变化的”,而不是把一堆硬编码的值散落在各个文件里。这种思维方式的转变,比工具本身更有价值。