做后端开发这些年,我发现自己最烦的从来不是复杂的业务逻辑,而是无穷无尽的模板代码。接口要写、字段要映射、异常要处理、算法模板更是几百行起步,这些东西结构固定、内容重复,但每写一次都要小心翼翼,漏改一个字段名,线上就给你颜色看。为了彻底解决这个问题,我折腾了一个可自定义规则的模板代码生成工具,用规则文件加模板文件的方式批量生成代码,简单、高效,而且完全不烧 token,也不依赖任何外部服务。
这个工具能做的事情一句话说清楚:你定义好规则和模板,它根据规则把模板里变化的部分替换成实际值,一次性生成完整的代码文件。适合几类人——经常写 CRUD 接口的后端开发、维护多套项目骨架的工程效率选手、比赛前需要反复手写大模板的算法竞赛党,以及所有被重复代码折磨又不想为此引入重型框架的人。如果你也有同样的困扰,这篇文章可以给你一整套可以直接落地的方案。
1. 模板代码为什么会成为负担:先从问题本身说起
1.1 模板代码的三种常见形态
很多人听到“模板代码”第一反应是代码生成器生成的那堆东西,但实际操作中,模板代码远比你想象的普遍。我自己的经验把它分成三类,每一类的痛法都不太一样。
第一类是项目骨架代码。新项目要建 Maven/Gradle 目录、要配 Swagger、要搭统一返回结构、要写分页工具类,这些代码在 A 项目和 B 项目里几乎一模一样,唯一的区别可能就是包名、项目名、几个核心类名不一样。过去我都是把上一个项目的配置文件直接复制过来,然后全局替换包名,替换完了还要担心有没有漏掉某个 yml 里的隐藏引用。
第二类是业务增删改查代码。后端写 Restful 接口,一个标准的服务模块要包含 Controller、Service、Mapper、DTO、VO、实体类这一整条链,单表 CRUD 五个接口就是十五六个方法。如果系统里有三十张表,这个数量就非常可观了。最崩溃的是数据结构调整之后,这三十个模块的公共字段都要跟着改一遍,纯手工维护基本是灾难。
第三类是算法竞赛里的固定模板。比如线段树套线段树、树链剖分、平衡树、网络流 Dinic,这些模板短则一百行、长则四五百行,而且核心结构非常固定,每次比赛前或者做新题时都要重新敲一遍。手打容易出错,用旧的又怕忘记改数据范围,这其实也是模板代码的一种,而且是最折磨人的那种。
这三类问题的共同点是:结构完全可预期,变化点就那么几个。真正花时间的地方不在“怎么写”,而在“怎么改对”。这正是模板代码生成工具能发力的地方。
1.2 为什么复制粘贴和 AI 生成都治标不治本
先说复制粘贴。它的问题不是不能用,而是不可维护。你从一个老项目里复制一份代码过来,全局替换关键字,当时可能没问题,但过两天发现漏掉了一个字段没改、一个路径没替换,排查成本比重新写一遍还高。而且全局替换这种操作本质上没有脱离“人肉”的范畴,它只是把错误从“写错”变成了“漏改”。
再来说说现在很多团队在用的 AI 生成。大模型确实能理解你的需求,给你产出一段结构完整的代码,但有几个现实问题。第一是接口调用要花钱,频繁生成代码的团队一个月 token 费用不低,很多人没意识到这块隐性成本。第二是生成结果不可控,同一个需求让模型生成三次,三次的代码风格、命名方式、结构层次都可能不一样,这给后续维护带来了很大麻烦。第三是算力消耗大,生成一个接口可能几十秒,并联交互占用的时间并不比手写短太多。
所以我在做工具的时候,理念是反过来的:能用规则描述清楚的东西,就不要让模型去“猜”,也不要在每次生成时重新“想”。模板代码的本质是确定性输出,不需要智能,需要的是稳定和一致。
1.3 工具的核心思路:用规则把“变”与“不变”拆开
写模板代码生成工具,核心思维只有一句话:把“变”和“不变”拆开。不变的是代码骨架——控制器怎么接收请求、服务层怎么定义接口、持久层怎么写 SQL,这些固定结构可以沉淀成一个模板文件;变化的是变量——类名、字段名、表名、注释、参数类型、返回类型,这些提炼成规则文件里的若干字段。
工具运行的时候,读入模板文件,读入规则文件,把模板中的占位符替换成规则中定义的值,最后输出一个完整可用的代码文件。听起来很简单,但正是这种“简单”保证了它的稳定。你不用每次让 AI 替你重新发明一遍 Controller 该长什么样,也不用担心人肉复制粘贴漏掉某个引用,因为模板定了,规则定了,输出一定是确定的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 模板代码生成工具的核心设计拆解
2.1 变量定义与占位符解析:模板的“填空题”
模板文件里最基础也最核心的机制是占位符。通俗理解,模板就是一张考试试卷,试卷的题目都印好了,只有括号里的内容需要你填,占位符就是这些括号。
我推荐用双花括号的形式,例如 {{ className }}、{{ packageName }}、{{ tableName }},这种做法的好处是视觉上非常显眼,不容易和模板里真实出现的代码语法冲突。解析的时候用正则匹配双花括号之间的变量名,再从规则上下文里取出对应的值进行替换。
这里有一个容易踩坑的地方:变量名命名要尽量具体。很多新手会写 {{ name }} 这种占位符,一旦模板文件超过两百行,你根本记不清这个 name 是指类名、方法名还是数据库名。我自己的习惯是统一用“模块语义 + 形容词”的方式,比如 {{ moduleName }}、{{ resourceUrl }}、{{ queryFieldType }},牺牲一点体积换来的是可读性和可维护性,非常值得。
实际实现时不需要做得太重,一个简单的工具只要做到两点就够了:第一,能识别模板中所有占位符;第二,能在替换前检查上下文里是否存在这个变量,如果变量缺失,直接报错而不是生成一个半成品。别小看第二个设计,它能帮你避免很多“生成完了发现一堆空字符串”的尴尬。
2.2 条件与循环:让模板具备判断能力
如果模板只有占位符替换,那它充其量算一个“批量替换工具”,离真正的代码生成还有距离。实际场景里,我们经常需要根据不同的输入条件输出不同的代码结构。
举个典型例子:生成一个接口,可能是新增操作,也可能是更新操作。新增操作需要校验唯一性,更新操作不需要;新增操作的默认字段是系统自动填充,更新操作允许用户传入。如果你要一个模板同时覆盖这两种场景,就必须支持条件判断。
我建议在模板里用 {% if %} 和 {% for %} 这样的控制标记。比如:
{% if needUniqueCheck %}
// 唯一性校验逻辑
{% for field in fields %}
// 遍历字段生成属性,例如:private {{ field.type }} {{ field.name }};
这样规则文件里只要配置 needUniqueCheck 的真假、fields 列表的内容,同一个模板就能应对不同需求,完全不需要维护两个模板文件。
循环的用处更大。生成 CRUD 代码时,实体类的属性、数据库字段的映射、表格的列定义,本质上都是“同一形式的重复”,用循环可以一句代码都不用改,就能从同样一套字段配置里生成多份配套代码。
2.3 自定义规则解析:不烧 token 的关键设计
“不烧 token”这个特性,本质上是说:这个工具不依赖任何大模型接口,运行在本机,消耗的只有一点点 CPU 和内存。它不是一个 AI 工具,而是一个确定性规则引擎。
规则文件我推荐用 YAML 格式,因为可读性好,嵌套结构清晰。和模板文件放在同一个目录下,文件名也保持一致,这样生成时只需要指定一个名字,工具就能自动找到模板和对应的规则文件。
规则文件的内容通常分为两块:全局配置和数据定义。全局配置包括包名、作者、生成日期、文件输出路径这些信息;数据定义则是本次生成的变量内容,比如字段列表、类名、表名。两块信息在模板里都有可能被引用。
设计上最重要的一点是:规则文件本身也是代码,需要纳入版本管理。很多人用代码生成器,只把生成的代码提交到 Git 里,模板和规则散落在本地磁盘,换台电脑就全没了,这是非常可惜的。我强烈建议把 templates 目录和 rules 目录作为项目的一部分统一管理,这样任何一次生成的代码都能追根溯源,知道它是从哪个模板、哪个规则来的。
3. 实操:跑通一个可自定义规则的模板代码生成工具
3.1 工具选型与基础实现
如果你完全从零开始,我会建议用 Python 写工具主体,模板解析直接用 Jinja2,规则解析用 PyYAML,几十行代码就能完成一个可用版本。Jinja2 本身支持变量替换、if/for 控制和过滤器,正好满足下面的需求。
先安装依赖:
bash复制pip install jinja2 pyyaml
工具的核心文件可以设计成下面这个结构:
text复制generator/
├── templates/
│ ├── controller_template.j2
│ ├── service_template.j2
│ └── algorithm_template.j2
├── rules/
│ ├── order_controller.yaml
│ ├── order_service.yaml
│ └── segment_tree_suite.yaml
├── output/
└── generate.py
generate.py 的骨架代码如下:
python复制import sys
from pathlib import Path
import yaml
from jinja2 import Environment, FileSystemLoader
TEMPLATE_DIR = Path(__file__).parent / "templates"
RULE_DIR = Path(__file__).parent / "rules"
OUTPUT_DIR = Path(__file__).parent / "output"
def generate(template_name: str, rule_name: str):
rule_path = RULE_DIR / f"{rule_name}.yaml"
if not rule_path.exists():
print(f"[错误] 找不到规则文件: {rule_path}")
sys.exit(1)
with open(rule_path, "r", encoding="utf-8") as f:
context = yaml.safe_load(f)
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
template = env.get_template(f"{template_name}.j2")
# 传入规则文件中的 data 部分作为模板上下文
content = template.render(**context.get("data", {}))
output_path = OUTPUT_DIR / context.get("output", {}).get("file", "output.txt")
OUTPUT_DIR.mkdir(exist_ok=True)
output_path.write_text(content, encoding="utf-8")
print(f"[成功] 已生成文件: {output_path}")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("用法: python generate.py <模板名> <规则名>")
sys.exit(1)
generate(sys.argv[1], sys.argv[2])
运行方式非常简单:
bash复制python generate.py controller_template order_controller
它会读取 templates/controller_template.j2 和 rules/order_controller.yaml,把生成结果写到 output 目录下。这个设计体量小、没有外部依赖、启动速度快,真正做到了“不烧 token”。
3.2 用模板生成“线段树套线段树”代码模板
要说模板代码生成工具最有价值的场景,我第一个想到的就是算法竞赛里的线段树套线段树。这个结构在竞赛圈大名鼎鼎——外层线段树维护序列下标,内层线段树维护值域信息,处理二维区间查询时非常强力,但代价是代码量极大,完整实现通常三百行起步,手打一遍至少半小时,还要担心变量名、边界条件、空间开够没有。
这种代码正是模板工具发挥神威的地方。你把固定的树结构、build、update、query 逻辑写进模板文件,把可变的部分提炼成规则配置,生成一次耗费的时间不到一秒钟,而且生成的代码完全基于你已经调试稳定的模板,正确率远高于现场手写。
模板文件的核心结构可以这样写:
jinja2复制// 线段树套线段树模板 - {{ name }}版
// 生成时间: {{ generate_time }}
#include <bits/stdc++.h>
using namespace std;
const int MAXN = {{ n_limit }};
const int MAXV = {{ value_limit }};
struct InnerTree {
int lc, rc;
{{ value_type }} mx;
} seg[MAXN * 4 * {{ inner_factor }}];
int tot = 0;
int new_node() {
return ++tot;
}
void push_up(int p) {
seg[p].mx = max(seg[seg[p].lc].mx, seg[seg[p].rc].mx);
}
void inner_update(int &p, int l, int r, int pos, {{ value_type }} val) {
if (!p) p = new_node();
if (l == r) {
seg[p].mx = max(seg[p].mx, val);
return;
}
int mid = (l + r) >> 1;
if (pos <= mid) inner_update(seg[p].lc, l, mid, pos, val);
else inner_update(seg[p].rc, mid + 1, r, pos, val);
push_up(p);
}
{{ value_type }} inner_query(int p, int l, int r, int ql, int qr) {
if (!p) return {{ default_value }};
if (ql <= l && r <= qr) return seg[p].mx;
int mid = (l + r) >> 1;
{{ value_type }} res = {{ default_value }};
if (ql <= mid) res = max(res, inner_query(seg[p].lc, l, mid, ql, qr));
if (qr > mid) res = max(res, inner_query(seg[p].rc, mid + 1, r, ql, qr));
return res;
}
// 外层线段树部分
int root[MAXN * 4];
void outer_update(int idx, int l, int r, int pos, int val_pos, {{ value_type }} val) {
inner_update(root[idx], 1, {{ value_limit }}, val_pos, val);
if (l == r) return;
int mid = (l + r) >> 1;
if (pos <= mid) outer_update(idx * 2, l, mid, pos, val_pos, val);
else outer_update(idx * 2 + 1, mid + 1, r, pos, val_pos, val);
}
{% if need_default_ctor %}
// 默认构造函数,方便外部直接声明对象
SegmentTree2D() { memset(root, 0, sizeof(root)); }
{% endif %}
规则文件 segment_tree_suite.yaml 可以这样写:
yaml复制output:
file: "segment_tree_2d.cpp"
data:
name: "动态开点树套树"
generate_time: "2025-01-01 10:00:00"
n_limit: "100010"
value_limit: "1000000"
value_type: "long long"
default_value: "-9e18"
inner_factor: "45"
need_default_ctor: true
执行一次生成:
bash复制python generate.py algorithm_template segment_tree_suite
秒级输出一个可以编译的 C++ 文件。用同一个模板,我还能生成 int 版本、double 版本、不同数据范围版本,唯一要改的就是 YAML 里的几个数值。这样到了比赛前夕,我可以给不同的模板批量生成“保险版本”放在本地,省去大量重复劳动。
3.3 接入日常开发流程
工具做好之后,接入日常流程的关键是“顺手”。如果每次生成代码要打开终端敲一长串命令行,用几次就会坚持不下来。我实际试下来,以下几个方式能让工具的使用成本降到最低。
第一是命令行缩短。把 generate.py 封装成 generate,放到 PATH 里,用的时候直接:
bash复制generate controller order_controller
generate algo segment_tree_suite
第二是利用 IDE 的外部工具配置。JetBrains 系 IDE 可以在 File -> Settings -> Tools -> External Tools 里配置一个自定义工具,参数用宏变量传入当前选中的文件,这样在编辑器里一键就能跑生成命令。配置关键参数如下:
| 配置项 | 值 |
|---|---|
| Program | python |
| Arguments | $FilePath$/../../generate.py $Prompt$ |
| Working directory | $ProjectFileDir$ |
| 建议勾选 | Always show output console,方便看生成日志 |
第三是模板文件的版本管理。我在所有项目的根目录都放一个 templates 目录,里面就是那三类模板。这样接手项目的新人不用问“你们的 Controller 是怎么写的”,看模板就全明白了,这比任何团队文档都直观。
4. 常见问题与排查技巧实录
4.1 变量替换不到:优先级和命名冲突
用模板工具最大的坑就是变量替换不到,生成出来的文件里还有一堆花括号,看起来非常崩溃。排查思路一般分三层。
先确认规则文件里确实定义了这个变量,很多时候是 YAML 里写错了缩进,字段名多了一层嵌套,导致模板拿不到。再确认变量名拼写完全一致,我在这场栽过几次,模板里写的 {{ class_name }},规则里却写的 className,下划线和驼峰对不上,肉眼很难发现。最后要确认模板里没有使用 Jinja2 的保留关键字,比如用 {{ loop }} 这种当变量名,会被直接吃掉。
建议在工具里加一个“模板渲染前打印变量表”的调试模式,把 context 里所有 key 按字母序输出,逐个比对比变量名。实际用下来这是最省时间的排查手段。
4.2 条件分支不生效:布尔值类型
YAML 里写布尔值有个坑:true 和 false 是布尔类型,但 "true" 和 "false" 加了引号就变成了字符串,Jinja2 里对这两者的处理结果完全不一样。{% if needDefault %} 这个分支,当 needDefault 是字符串 "false" 时,Jinja2 会把它当成非空字符串,判断为真,于是不该生成的代码被生成出来了。
解决方式很简单:规则文件里写布尔值一律不加引号;模板里尽量用 {% if needDefault is defined and needDefault %} 这样带着 is defined 判断,避免因为变量不存在而漏掉分支。
4.3 复杂嵌套模板的缩进与空格问题
处理 C++ 这种对布局有要求的语言时,模板里的缩进会比预想中更难控制。Jinja2 默认会保留模板文件里控制标记前后的一切空白,如果你的模板写成下面这样:
jinja2复制{% if need_extra %}
int extra_func() {
return 0;
}{% endif %}
生成的代码布局会非常混乱,条件块后面的内容可能和前面的缩进对不上。解决办法是给控制标记加上减号:
jinja2复制{% if need_extra -%}
int extra_func() {
return 0;
}
{%- endif %}
减号的作用是去掉标记与相邻内容之间的空白,实际效果是输出内容时不会留下多余空行或缩进。我习惯在所有模板的控制标记都用这个写法,宁可多写两个减号,也不要生成出格式凌乱的代码。
4.4 与 AI 工具配合:什么时候用模板、什么时候用 AI
最后一点比较个人的建议。有人问我说,既然现在 AI 这么强,为什么还要用模板生成工具?我的回答是:AI 适合“非确定性需求”,模板工具适合“确定性需求”。第一次写一个全新模块的设计,可以让 AI 给你一个思路参考;但当你已经明确要生成一个标准 Controller、一个树套树模板、一个分页查询实现时,模板工具的产出更稳定、更一致,还没有 token 成本。
我实际的工作流是:用模板工具出基础骨架,用 AI 来辅助处理模板覆盖不了的特殊逻辑。这样两边配合,既避免了重复劳动,又不会陷入模板灵活性不足的困境。
5. 给你几个沉淀模板规则的小建议
工具本身写起来容易,真正要花心思的是沉淀模板规则。我自己总结了几条经验,你可以直接拿去用。
先把最常用的代码分好类,每一类只做一套标准模板。不要一上来就贪多,先选后端 CRUD 或者算法模板中你最头疼的一类,写完跑顺,形成习惯后再慢慢补充别的模板。其次是模板文件里的变量命名和规则文件里的字段命名要保持统一,建议命名规范写进 README,防止半个月后忘掉当初为什么叫这个名字。
另外一定要在模板顶部写清楚使用说明和版本历史,哪怕只有两三行注释,也能让你半年后重新看模板的时候省去大量回忆时间。最后就是生成结果不要直接扔进代码库,生成完一定要做代码 review,把工具当成效率助手而不是完全替代人脑。用工具节省下来的时间,应该花在更有价值的逻辑设计上。
我折腾这套东西最大的体会是:工具不是越复杂越好,而是越贴合自己的工作流越好。适合你的,就是最好的。
