资讯动态

轻量级代码生成器设计实践:用模板与模型分离消除重复CRUD

发布时间:2026/10/26 8:53:28 来源:尧图企业网站定制
前阵子整理工位的时候翻出来一个老项目叫t3code。我心里一动把它重新拉起来跑了一遍顺手把之前一直没来得及处理的几个坑也填了。这名字很简单——template three tiers code当初起名的时候就是想做一个“三段式”的轻量代码生成器模板、模型、输出各管一段。跟市面上那些动不动就来一套图形界面、在线预览、微服务扩展点的重型脚手架不一样t3code 就是个命令行工具跑起来不花哨但真正能把我从每天的重复 CRUD 里捞出来。这个东西适合谁如果你平时要写大量的实体类、接口定义、数据库访问层、前端列表页和表单页或者你所在的团队还没有统一代码生成规范程序员各自用各自的办法在复制粘贴那这篇内容对你会有用。如果你是那种喜欢“一刀切”生成器、希望一个命令产出完整可运行项目的用户t3code 会不太适合它的定位是“解决局部重复”不搞“全家桶”。我把实际用下来的设计思路、配置方式、踩坑经验全部整理到这篇文章里涉及的模板语法、配置文件写法、生成策略都是可以直接抄去用的。1. 需求审视与整体设计思路先聊清楚一个前提问题代码生成器到底解决什么问题我自己的答案是——消除结构性重复而不是消除思考。同样一组字段在数据库里建表要写一遍Java 实体里要再写一遍DTO 里换个注解又要一遍前端表格列再来一遍表单校验规则还得再来一遍。这种重复最可怕的地方在于任何一次改动都要同步改好几处漏掉一个就等着线上数据出问题。1.1 立项背景为什么不做大而全的“脚手架全家桶”做之前我也调研过不少现成方案。有些生成器功能确实强支持从数据库反向建模还能直接生成前端路由、状态管理、接口封装看起来无所不能。但用了两三个项目之后我发现几个尴尬的问题。第一学习成本太高光配置项就上百个团队里不是每个人都能记住那些概念第二生成结果“过重”它默认帮你把项目结构也拉起来了可很多老项目根本不允许你把整个架构推倒重来第三自定义能力受限你改模板需要理解它的抽象概念否则只能在生成的代码上手动补丁。所以 t3code 立项时我把目标压得很窄只要把“一类代码”的生成做好就够了。比如这一趟专门生成接口层的 CRUD 模板下一趟专门生成前端表格页模板模块之间互相独立配置文件互相隔离。这样有个好处每套模板的维护成本非常低改一个生成规则影响的只是这一类代码不会波及全局。1.2 核心设计原则模板与数据分离代码与配置分离t3code 的目录结构是这样的。根目录下一个templates文件夹专门放模板文件一个models文件夹放数据描述文件output文件夹放生成结果。模板里面不写死任何业务字段只写变量占位符和逻辑控制语句模型文件里只描述“有几个字段、各自什么类型、有哪些约束”完全不涉及输出格式。两者通过一条命令关联起来。这个分离思路是从写页面的时候学来的。做前端的时候我们讲模板和数据分离服务端渲染也好、客户端渲染也好只要看见模板里塞着写死的数据后面维护就是灾难。代码生成器也一样——模板一旦混入具体表字段这个模板就废了换一个表立刻就得复制一份改半天。所以 t3code 严格要求模板文件里只出现占位符任何一个具体字段名都不允许出现在模板里。1.3 技术选型背后的取舍实现语言我选了 Python没有太多花哨理由团队里所有人都会一点跨平台也没问题字符串处理能力又强。模板引擎选了 Jinja2这是 Python 生态里最成熟的方案语法对熟悉服务端开发的人几乎零门槛{{ }}插值、{% for %}循环、{% if %}条件干净清爽。配置文件格式用 YAML比起 JSON 少写一堆逗号括号比起 XML 又不那么啰嗦嵌套表达列表和字典都很自然。这里有个取舍想单独说说。有人可能觉得用 JavaScript 写会更现代Node.js 生态里的 EJS、Handlebars 也很成熟。但我坚持用 Python 的原因只有一个生成代码这个场景下 Python 处理路径、编码、文件操作极其顺手。比如 Windows 下文件名编码、路径拼接那些坑Python 标准库基本都帮你兜住了。工具是拿来用的不是拿来炫技的选择团队最顺手的技术让维护成本降到最低这才是这类工具的长久之道。2. 核心模块拆解与关键技术实现把 t3code 拆开看其实就三个核心模块模型解析器、模板渲染器、文件写出器。听起来都不复杂但每个模块里都有一些容易被忽略的细节这些细节恰恰决定了这个生成器是“能用”还是“好用”。2.1 模型解析器一切生成的基础模型文件是这个工具的“灵魂”描述的是业务结构不掺任何展示逻辑。我用的 YAML 格式来写比如描述一张用户表大概长这样table: user comment: 用户信息表 fields: - name: id type: bigint comment: 主键ID primary: true autoIncrement: true - name: username type: varchar(64) comment: 登录名 nullable: false length: 64 form: input - name: status type: tinyint comment: 状态 0-禁用 1-启用 default: 1 form: radio options: - { label: 禁用, value: 0 } - { label: 启用, value: 1 }模型解析器要做的事不只是读 YAML 这么简单。它还要做类型映射比如把数据库类型varchar(64)映射成 Java 的String把bigint映射成Long或者映射成 TypeScript 的number。这些映射规则集中在单独的配置里不写死在代码里这样换一套技术栈只需要改映射表模型文件不需要动。字段属性这块我想多讲一句。很多人写代码生成器字段只描述“名称类型注释”结果生成出来的前端表单永远需要手工再补很多东西。我在 t3code 里给每个字段预留了form参数可以标记这个字段在前端用什么组件渲染——input、textarea、radio、select、date还能挂options选项列表。这样生成前端表单的时候就能自动选择对应的组件校验规则也能跟着生成。模型描述得越细生成结果就越接近最终想要的效果。2.2 模板渲染器从变量插值到逻辑控制模板引擎这块是整个系统的核心Jinja2 的能力要为生成代码这个场景做好取舍。变量插值是最基础的能力比如实体类模板里常见这么一段{% for field in fields %} /** {{ field.comment }} */ private {{ field.javaType }} {{ field.name }}; {% endfor %}这段模板会把模型文件里的字段列表循环展开每个字段生成一段带注释的 Java 属性声明。Jinja2 的语法本身就是为这种场景设计的循环里还能做条件判断比如主键字段自动加Id注解时间字段自动加JsonFormat注解。模板渲染器还有一个容易被忽略的功能叫空白控制。Jinja2 默认会在标签位置留下多余的空行生成的代码看起来一言难尽。我用的是这种写法{% for field in fields -%} private {{ field.javaType }} {{ field.name }}; {%- endfor %}标签后面加一个-就能吞掉相邻的空格和换行。这个小技巧单独拿出来说是因为很多人初次写代码模板最大的困惑不是逻辑写不出来而是生成的代码格式乱到没法看。模板继承功能也值得讲。比如 Controller 层和 Service 层的模板结构高度相似就可以抽一个公共的 base 模板通过{% extends %}和{% block %}来复用公共部分。我实际用下来代码模板的复用率比想象中要高得多——毕竟一个项目的分层结构是稳定的变来变去的只是业务字段而已。2.3 文件写出策略覆盖、跳过与补丁生成代码之后怎么落地这是个非常现实的问题。最粗暴的做法是每次生成直接覆盖同名文件但实际项目中生成的代码往往会被手动改过——比如某个 Controller 方法你加了缓存逻辑、某个页面你加了自定义交互。如果直接覆盖这些手写内容全部丢失。所以我设计了三种写出模式。第一种是overwrite模式直接覆盖目标文件适合那些“生成之后绝不会手动改”的文件比如纯 DTO、VO 这一类。第二种是skip模式如果文件已存在则跳过适合配置文件这类初始化一次就不该被反复生成的场景。第三种是merge模式这个模式最复杂我用的策略是在手写文件和生成文件之间做“标记位”的合并模板里用指定注释把“自动生成区”和“手写保留区”隔离比如 Java 文件里用// AUTO-GENERATED BEGIN // AUTO-GENERATED END 生成器只重写这个标记区间内的内容区间外的手写代码原样保留。这套方案确实有效但要求模板作者有很强的自律性必须保证手写代码写不进生成区否则一覆盖就没了。后来我简化了一下大多数场景下直接采用“生成到独立文件 手动复制少量内容”的方式反而比强求自动化更省心。2.4 命令行交互设计t3code 的命令行入口是我花心思比较多的地方。没有做复杂的参数解析库就用了 Python 标准库argparse命令长这样t3code gen -t templates/entity.j2 -m models/user.yaml -o output/java/-t指定模板文件-m指定模型文件-o指定输出目录。参数越少越好几个固定的槽位就够不做花哨功能。我用了一段时间后又加了一个--list参数专门列出当前项目下所有可用的模板和模型方便团队其他人快速上手不用去翻文档。后来我还加了一个交互式引导在没有任何参数的时候进入问答模式一个个问“模板用哪个模型用哪个输出到哪个目录”这样新同事第一次用的成本几乎为零不用看任何说明文档。这也是我踩过坑之后才加上的——刚做完 t3code 的时候团队里根本没人愿意用他们说“还得记参数还不如自己复制粘贴”后来加了引导问答情况立刻好转。工具要让人愿意用体验和功能同等重要。3. 实操过程从零配置一个完整的 CRUD 模块讲了大半天理论不如看个完整的案例。这一步我拿“权限管理”里最常见的角色表来做演示从模型编写、模板制作到最终生成代码整个链路走一遍。看完之后你可以拿着这个流程照搬到自己的业务模块上。3.1 编写角色表的模型描述文件角色表sys_role结构很简单主键、角色编码、角色名称、状态、排序、备注、创建时间、更新时间。我整理成如下的 YAML 保存到models/sys_role.yamltable: sys_role comment: 系统角色表 javaPackage: com.example.admin.entity fields: - name: id type: bigint comment: 角色ID primary: true autoIncrement: true - name: role_code type: varchar(64) comment: 角色编码例如 admin nullable: false unique: true length: 64 form: input - name: role_name type: varchar(64) comment: 角色名称 nullable: false length: 64 form: input - name: sort type: int comment: 排序值越小越靠前 default: 0 form: number - name: status type: tinyint comment: 状态 0-禁用 1-启用 default: 1 form: radio options: - { label: 禁用, value: 0 } - { label: 启用, value: 1 } - name: remark type: varchar(255) comment: 备注 nullable: true length: 255 form: textarea - name: create_time type: datetime comment: 创建时间 nullable: true form: datetime - name: update_time type: datetime comment: 更新时间 nullable: true form: datetime注意几个细节。第一凡是数据库里可能为空的字段我都在nullable: true里面标注了这样后面生成校验注解时能自动区分必填和非必填。第二每个字段都补了form参数标明表单组的类型这样前端模板就能直接选取对应的组件。第三排序、状态这类字段单独标注了默认值为后面生成“新增接口”提供默认参数。3.2 制作实体类模板实体类模板放在templates/java_entity.j2内容是这样的package {{ javaPackage }}; import lombok.Data; import javax.persistence.*; import java.time.LocalDateTime; Data Table(name {{ table }}) public class {{ entityName }} { {% for field in fields %} Column(name {{ field.name }}) {% if field.primary %}Id{% endif %} {% if field.autoIncrement %}GeneratedValue(strategy GenerationType.IDENTITY){% endif %} private {{ field.javaType }} {{ field.name }}; {% endfor %} }这个模板里用了一个模型里没有直接给出的变量entityName。它是由sys_role转换来的驼峰写法SysRole。这种“命名转换”是模板引擎的常用手法t3code 在渲染之前会自动给模型对象注入这类派生属性把表名转类名、转字段名、转首字母小写格式。模板里就不用写复杂的转换逻辑了只管消费这些现成的变量。类型映射配置放在configs/type_map.yaml里关键几行是这样java: bigint: Long varchar: String int: Integer tinyint: Integer datetime: LocalDateTime decimal: BigDecimal这个配置文件独立于模板和模型想切换数据类型映射规则的时候只改这里不需要动其他任何地方。3.3 生成 VO、DTO 与查询参数实际项目里很少直接拿实体类去接收前端的请求参数通常还要有入参对象和出参对象。这类类结构高度相似可以共用一个模板通过传入参数来区别文件命名。我写了templates/java_vo.j2package {{ javaPackage }}; import lombok.Data; {% for field in fields %} {% if field.fieldValidate %} import javax.validation.constraints.*; {% endif %} {% endfor %} import java.io.Serializable; Data public class {{ className }} implements Serializable { {% for field in fields %} {% if field.nullable false %} NotNull(message {{ field.comment }}不能为空) {% endif %} {% if field.length and field.length 0 %} Size(max {{ field.length }}, message {{ field.comment }}长度不能超过{{ field.length }}) {% endif %} private {{ field.javaType }} {{ field.name }}; {% endfor %} }注意到没有模板里用了field.nullable和field.length这两个属性去做逻辑判断自动给字段追加校验注解。这比手写每个字段的校验要高效得多而且改模型描述的时候校验规则自动跟着变一致性天然有保障。生成命令是t3code gen -t templates/java_vo.j2 -m models/sys_role.yaml -o output/java/vo/ \ --var classNameSysRoleVO--var参数可以在命令行额外指定模板里缺失的变量这个设计很灵活能满足不同场景下对类名、包名的特殊要求。3.4 生成前端列表页与表单页后端类生成完之后前端页面才是重头戏。早期我手写管理后台的列表页一个字段一个瞎写光是表格列就得磨上半天。后来用 t3code 做了个 Vue 3 Element Plus 风格的列表页模板写一次模板所有列表页都自动化了。模板核心片段长这样template div classpage-container el-card el-form inline {% for field in fields %} {% if field.formQuery %} el-form-item label{{ field.comment }} {% if field.form input %} el-input v-modelqueryParams.{{ field.name }} placeholder请输入{{ field.comment }} clearable / {% elif field.form select or field.form radio %} el-select v-modelqueryParams.{{ field.name }} placeholder请选择{{ field.comment }} clearable el-option v-foritem in {{ field.name }}Options :keyitem.value :labelitem.label :valueitem.value / /el-select {% endif %} /el-form-item {% endif %} {% endfor %} el-form-item el-button typeprimary clickhandleQuery查询/el-button /el-form-item /el-form el-table :datatableData stripe {% for field in fields %} el-table-column prop{{ field.name }} label{{ field.comment }} / {% endfor %} el-table-column label操作 width200 template #defaultscope el-button typeprimary link clickhandleEdit(scope.row)编辑/el-button el-button typedanger link clickhandleDelete(scope.row)删除/el-button /template /el-table-column /el-table /el-card /div /template这里模板里用到field.formQuery这个属性是模型解析阶段根据字段属性自动推断的。主键、时间字段默认不进查询条件状态、编码这类才进入查询区。这种方式的好处是前端模板也是一个通用的“渲染规则”规则集中维护改一处全局生效。生成前端文件t3code gen -t templates/vue_list.j2 -m models/sys_role.yaml -o output/web/views/sys/ \ --var pageNamerole一次生成得到的 index.vue 里表格列、查询条件、编辑按钮都齐了手动要做的事情基本只剩接入接口 API。3.5 生成之后的人工整合环节必须承认任何代码生成器都不可能完全免掉人工操作。t3code 聚焦的是“结构化代码的批量产出”生成完之后你仍然需要做这几件事把生成的文件按项目分层结构放进正确的目录把对应的路由注册配置补上如果模板没有覆盖到特殊交互得在生成的代码基础上继续写。我自己一般约定“文件落盘后默认进入人工复核阶段”。生成器负责把重复劳动降到最低但业务逻辑的判断权必须留在人这里。比如列表页的删除按钮如果角色被用户绑定删除前需要二次确认并调后端检查接口这种逻辑就要手写但手写成本远比从零开始写低得多——因为表格、表单、弹窗这些骨架已经都齐了。4. 实际使用中的常见问题与排查记录用了大半年t3code 帮团队省了非常多的时间也积累了一堆问题和对应的处理办法。这部分挑几个典型问题写出来都是实际踩过的坑。4.1 模板变量名冲突模板文件里名为name或者type的变量非常容易跟 Jinja2 的内置变量或者模型自身的默认属性重名。有一次我在模板里写了{{ type }}想渲染字段类型结果渲染出来的是模型的顶层类型标识排查了半天才发现是变量冲突。后来我明确规定模型属性统一加前缀f_比如f_name、f_type、f_comment模板里永远使用全名引用。这个看似有点麻烦的规范直接消灭了所有类似问题。4.2 空白控制导致的格式问题前面提到过 Jinja2 的空白控制。使用{%- -%}这种语法的时候只要有一个地方少写了-生成的代码就会多出无数空行检查起来非常痛苦。我的排查经验是先关掉所有空白控制看看哪个标签附近出现多余空行再针对性修改这个位置的语法。别全模板替换容易产生新的格式问题。这个排查思路虽然简单但能省很多时间。4.3 文件覆盖误伤这是使用成本最高的一类问题。生成的 Service 实现类被手写业务逻辑填满了结果一次误操作重新生成全部被覆盖回初始状态。这个问题的根源在于我早期所有文件都用了overwrite模式。后来对写策略做了调整只有纯数据类文件允许覆盖凡是“人类需要持续写入”的文件一律用skip模式或者只生成到临时目录确认无误后手工合并。宁可多一步人工操作也绝不能让生成器毁掉手写代码。4.4 模板维护不及时导致的不一致团队里有几个人各自维护不同模板改了一处没同步另一处导致不同模块生成的代码风格不统一。后来我把模板仓库集中到 Git 管理并约定任何模板修改必须过代码评审。这听起来有点行政化但实际的收益非常显著——模板是团队的公共资产它决定了线上代码长什么样这类资产的变更质量比业务代码变更更值得守规矩。4.5 类型映射遗漏数据库里有json类型字段而类型映射表里没有配结果生成出来的 Java 属性是 Object前端渲染直接报错。我现在的类型映射表里内置了常见数据库类型的默认映射同时支持用户在映射配置里追加自定义规则。排查思路就是生成完代码后全局搜一遍Object或者any这类“兜底类型”凡是出现这类字样的字段一定是类型映射没配好。我把这些问题整理成一张速查表给团队新人也用得上常见问题典型原因快速处理办法生成文件格式乱模板空白控制不当单独检查每个{% %}附近的空白控制-标记变量渲染成奇怪值变量名与内置属性冲突模型属性统一加f_前缀禁用裸变量名手写代码被覆盖文件策略选了 overwrite改为 merge 或手动合并模式类型映射缺失type_map 未配置搜索兜底类型全局排查模板修改影响范围大模板拆分不够细一个模板只负责一类代码保持小粒度5. 从 t3code 延伸出的思考与个人经验工具本身讲得差不多了最后聊聊方法论层面的一些体会。做代码生成器这件事最大的陷阱不是技术实现而是“边界感”。5.1 生成器的边界在哪坦白说我早期犯过一个很严重的错误想把所有东西都塞进生成器里包括鉴权逻辑、日志切面、分布式事务回调。结果模板越写越复杂模型文件里塞满了跟业务字段无关的框架参数生成的代码可读性越来越差最后退化成“生成一下然后推倒重写”。这个教训让我明白生成器应该只负责“结构明确、规则固定”的那部分代码生成凡是“需要业务灵感、需要上下文判断”的逻辑都应该留给人工。具体到这个案例角色管理模块里的“新增角色时初始化角色数据权限”这种逻辑就不应该进模板。模板里的东西应该是恒定的结构而业务规则是变动的语义。这两者混在一起模板就失去复用价值了。5.2 小步快跑的迭代方式如果你也想做一个类似的工具我强烈建议你不要一开始就想“做一个完整的生成器平台”而是从一个最迫切的痛点出发比如“这周又要写 5 张表的 CRUD”那就先做一个实体类生成器解决这一件事。跑顺之后再慢慢往里补 VO 生成、补前端表格、补表单页面。t3code 的每个阶段新增功能都不大但每一步都解决了实实在在的问题工具是跟着需求长出来的不是一次性设计出来的。5.3 模板与模型的资产化意识代码生成器的真正资产不是那份工具代码而是沉淀下来的模板和模型描述文件。新项目进来只需要把模型文件写出来模板一套代码自动就有雏形。模型文件描述的是业务结构它本身就是一份高质量的、可读性很好的领域文档。这个资产的价值不是一次性的它会随着每个新项目的推进而不断积累。等到模板库覆盖了你团队常见的技术栈和业务模式新人上手项目的速度会有质的提升。关于模板管理我还想提一个细节一定要为模板保留版本记录。团队里发生过因为改了模板顺手改了命名规范导致旧模块重新生成时代码风格跟历史代码不一致。有了模板版本记录回溯和对比的成本就会小很多。5.4 后续可能的扩展方向目前 t3code 停留在“本地命令行工具”的阶段后续有些方向值得考虑。比如接入数据库元数据自动生成模型描述文件省去手工写 YAML 的步骤再比如接上 Git 预提交钩子代码生成后自动跑一遍格式化命令又或者做一个简单的 Web 界面让非技术团队也能填写模型文件生成简单页面。这些都是合理的扩展方向但要记住前面讲的边界原则——每一步扩展都应该先回答一个问题这个功能到底能不能显著减少重复劳动如果不能就不做。做工具和做业务代码是两种不同的思维模式。写业务代码关注的是“把这个功能实现出来”做工具关注的是“如何让一类功能重复实现的成本无限趋近于零”。两者没有高下之分但思维方式差异很大。t3code 这个项目的意义不在于他本身有多复杂而在于它帮我把很多已经写过的代码从脑海里搬了出来变成了可以反复调用的规则。这种感觉跟第一次封装出自己的前端组件库时有点像——原来重复的工作堆在一起居然能沉淀出一点可以复用的东西。如果你现在正被大量结构相似、内容重复的代码折磨不要急着复制粘贴先停下来想想这部分重复能不能抽象成模板需要变化的点能不能抽成参数如果可以花半天时间做一个简单的生成器收益能持续到下一个项目。t3code 的代码量不大核心逻辑可能比你想象中还要简单但它带来的效率提升是实打实的。工具不需要有多大的野心能持续解决一个问题就是好工具。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑