
前阵子帮朋友排查一个STM32工程的诡异问题现象是“串口输出乱码且系统时钟明显不对”查了半天最后发现他把上一份工程里的宏定义原封不动复制过来了芯片型号和晶振频率都是旧项目的。这种问题几乎每个人都遇到过复制粘贴是项目初始化的最快方式但也是最容易翻车的方式。如果你也有这种“复制了三五个项目每次都改漏东西”的经历那你需要的其实是一个模板代码生成工具。模板代码生成工具说白了就是“把带占位符的文件通过数据渲染变成真正能用的代码、文档或项目骨架”的工具。它并不神秘很多你每天都在用的功能本身就是模板代码生成——IDEA里新建一个类自动带版权头这就是Angular/React脚手架帮你生成整个项目这也是甚至你用Latex模板写论文、用PPT模板做汇报底层逻辑都和模板代码生成一模一样。这篇文章不打算讲抽象概念就按我实际使用这些工具的经验来聊它到底解决了什么问题、市面上有哪些流派、怎么手写一个最小可用的生成器以及嵌入式工程、IDE注释模板、Excel数据驱动后台系统这几个典型场景怎么落地。最后再把踩过几次的坑拉出来说一遍希望你能少走弯路。1. 模板代码生成工具不是“复制粘贴”而是变与不变的分离1.1 复制粘贴的隐性成本其实很高我见过太多人初始化新项目的流程是从旧项目复制整个目录然后全局替换项目名再改一改关键配置。看起来很快但隐性成本极高。首先是漏改风险像开头说的宏定义几乎是复制粘贴最容易翻车的地方。其次是路径和文件名你复制一个Java项目里面包名、目录层级如果没改干净编译能过跑起来就404。再一个是规范问题三个人复制出三种风格的代码注释日期不一致、版权头缺失、目录结构混乱。这些不是操作者不细心而是人脑根本不适合处理这种“高重复、低判断”的工作。模板代码生成工具解决的正是这个问题。它的核心思路不是“帮你复制”而是先把文件里的内容拆成两部分永远不变的固定文本比如工程目录结构、代码中的公共逻辑、注释头、许可证声明以及每次生成时可能变化的部分比如项目名、包名、MCU型号、表名字段名、作者日期。固定文本写一次变化部分通过变量注入一次渲染一次得到完整干净的输出。1.2 核心思路把“变量”和“固定文本”分开如果你写过一点前端一定见过ES6的模板字符串const greeting Hello, ${name}!;这其实就是最最小化的模板代码生成Hello,是固定文本${name}是占位符运行时把name变量填进去。专业一点的模板引擎后面会细说在此基础上加入了循环、条件判断、过滤器、嵌套片段但本质还是这一句话生成的内容 固定模板 上下文数据。拿一个嵌入式工程来举例。固定的部分是启动文件结构、标准库的头文件包含、外设初始化流程、Makefile或Keil工程配置变化的部分是项目名、目标芯片STM32F103C8T6还是F407、时钟频率、引脚的复用功能、用不用RTOS。模板代码生成工具就是把这些“变化的部分”抽出来放到一个配置文件或者几个命令行参数里然后一次性渲染出整个可编译的工程。1.3 谁最需要模板代码生成工具我的经验是下面这些人最容易从中受益一个人维护多个项目且项目之间骨架高度相似嵌入式、后端服务、前端应用都是典型团队想统一代码规范、工程规范但没有精力靠人肉检查新项目或新模块启动频率高每两周就要建一次工程有跨语言、跨平台需求同一份数据模型要生成Java实体、Vue页面、SQL脚本、接口文档。如果你只是偶尔新建一个文件那确实没必要搭生成器但只要“重复三次以上”的动作出现就值得考虑模板化。2. 先分清三大流派模板引擎、脚手架生成器、IDE内建模板2.1 文本模板引擎以Jinja2、FreeMarker为代表这类工具的核心能力是“把模板文件渲染成目标文本文件”。你给一份模板、一份数据它输出最终文件。典型代表有Python生态的Jinja2、Java生态的FreeMarker和Velocity、Node生态的EJS和Handlebars。它们最大的优点是不挑场景。源码文件、配置文件、文档、SQL脚本、XML、Markdown只要是文本都能生成。正因为这个通用性文本模板引擎是绝大多数自定义代码生成器的基础——我自己写的生成工具基本都是用Python Jinja2再套一层自己的脚本逻辑。2.2 脚手架生成器Cookiecutter、Yeoman、Plop脚手架生成器比纯模板引擎更进一步它负责“生成整个项目目录结构”通常还带交互式问答。Cookiecutter是Python圈最出名的Yeoman是前端圈的Plop则轻量一些。这类工具的特点是把目录名、文件名本身也当成变量。比如你想创建一个项目它会先问你project_name和module_name然后按照模板目录生成{{cookiecutter.project_name}}/ ├── src/ │ ├── {{cookiecutter.module_name}}/ │ └── main.py └── README.md交互式问答比写配置文件对普通用户更友好而且可以设置默认值一路回车也能生成一个可用项目。我个人的习惯是如果生成的是整套项目骨架用Cookiecutter如果只是在一个已有仓库里生成部分模块用Plop更轻量。2.3 IDE内建模板IDEA Live Templates、VS Code Snippets这类模板被很多人忽略了但它们确实是“模板代码生成工具”的一种。IDEA里的Live Templates让你输入一个缩写再按Tab就能展开一大段代码模板甚至可以用函数动态填充参数、方法名、返回值。VS Code的Snippets同理。它们的优点是零额外依赖、直接嵌入编辑器缺点是各自为政换IDE就要重新配置一遍。但它们非常适合做“高频代码片段”的模板化。比如方法注释、测试用例、DTO转换、日志打印这些动作不需要建一个外部生成器直接用IDE模板效率最高。2.4 选型对比到底该用哪一种下面是我在实际选型时用的判断表按场景直接查维度文本模板引擎脚手架生成器IDE内建模板典型工具Jinja2, FreeMarker, EJSCookiecutter, Yeoman, PlopIDEA Live Templates, VS Code Snippets最佳场景生成单个/批量的源码、配置文件生成整套新项目/模块骨架编辑器内高频代码片段上手难度中需要会写脚本低交互式提问为主低图形界面配置扩展性极高可以任意组合高支持钩子脚本低受IDE功能限制是否适合团队统一适合模板可放仓库适合模板可放仓库一般需要手动导入配置典型输出main.c, POJO, SQL, Vue页面完整工程目录注释片段、类文件头我实际项目中经常是三者混用项目级骨架用Cookiecutter文件级生成用自写的Jinja2脚本编辑器里写码时的片段用Live Templates。三者各管一段互不冲突。3. 手写一个最小可用的模板代码生成器原理与落地3.1 模板的三个基本构成不管用哪种引擎模板代码生成器都跑不出三个构成占位符用特定语法标记要插入变量的位置比如Jinja2的{{ project_name }}控制结构条件、循环让模板能根据数据量或分支动态变化如{% if use_rtos %}、{% for item in fields %}渲染上下文一份数据对象字典、JSON、YAML或数据库查询结果渲染时传给模板引擎。很多刚上手的人会把模板写成“巨复杂的一行装配”然后在模板里写一大堆逻辑。我的建议反而不是这样模板里能不放逻辑就不要放逻辑复杂分支和校验放到生成器脚本里模板只负责“按上下文输出文本”。这样模板更容易读也更容易维护。3.2 一个能直接跑的示例用Jinja2生成Java类我最小的一套生成器通常就两个文件一个模板文件一个脚本文件。下面这个例子生成一个带类注释和字段注释的Java DTO。模板文件dto.java.j2package {{ package_name }}; /** * {{ class_comment }} * * author {{ author }} * date {{ date }} */ public class {{ class_name }} { {% for field in fields %} /** {{ field.comment }} */ private {{ field.type }} {{ field.name }}; {% endfor %} }生成脚本generate_dto.pyfrom datetime import date from jinja2 import Environment, FileSystemLoader, StrictUndefined env Environment( loaderFileSystemLoader(templates), trim_blocksTrue, lstrip_blocksTrue, undefinedStrictUndefined ) template env.get_template(dto.java.j2) data { package_name: com.example.dto, class_comment: 用户信息DTO, class_name: UserDTO, author: YourName, date: date.today().isoformat(), fields: [ {name: userId, type: Long, comment: 用户ID}, {name: userName, type: String, comment: 用户名}, ], } print(template.render(data))运行这个脚本会输出一个完整的Java类。如果把这个输出重定向到文件再套一层目录生成逻辑就是一个最简单的代码生成器了。不要小看这一段它已经覆盖了模板代码生成的所有核心步骤后面再怎么加功能底层都是这套逻辑。这里有几个细节值得注意trim_blocksTrue和lstrip_blocksTrue可以避免Jinja2的控制语句产生多余空行和缩进生成出来的代码干净很多StrictUndefined会在模板里引用了不存在的变量时直接报错而不是静默输出空字符串这个对排查问题非常有帮助。3.3 把文件名和目录也当成变量生成单个文件很容易但实际中我们更多是“生成一组文件”。这就要把模板和文件路径都变量化。一个典型的Jinja2文件加载器基于目录你可以在模板目录里创建templates/ ├── {{ module_name }}/Controller.java.j2 ├── {{ module_name }}/Service.java.j2 └── {{ module_name }}/Mapper.java.j2然后在脚本里遍历这些文件路径逐一对module_name赋值。用Cookiecutter就更方便它直接把目录名和文件名都作为模板变量渲染连目录结构都不用手动拼接。这就是脚手架生成器比纯模板引擎更适合做工程骨架的原因。3.4 模板里的逻辑要克制很多写代码的人一拿到模板引擎容易控制不住在模板里写复杂逻辑。比如在Jinja2模板里搞一个几十层的{% if %}嵌套或者用set重新计算一堆派生变量。我见过一个同事把一个订单状态流转逻辑写进了FreeMarker模板调试的时候痛不欲生。我的原则是模板只做“可变位置的插入”不做“业务计算”。需要算出来的值在生成器脚本里提前算好作为上下文传入。必要的时候可以在模板里用过滤器做格式化但不超过一层。这样模板看起来就是一份“带洞的最终文件”别人一眼能看懂输出长什么样。4. 实战场景一用模板生成STM32F103C8T6标准库工程骨架4.1 为什么嵌入式工程非常适合模板化嵌入式新项目的启动成本在各类开发里算是偏高的尤其是基于标准库做STM32F103C8T6开发。你要复制标准库要选启动文件要配置Keil5的Target、Options、C/C宏定义、Include路径还要写系统时钟初始化、GPIO初始化、串口重定向。这些步骤大多数项目完全一样只有引脚、时钟频率、外设组合不同。最常规的做法是“新建一个从旧项目复制出来的工程模板文件手动修修改改”这其实就是最原始的模板代码生成只不过全部靠手工变量替换。既然你用电脑开发为什么不把这个过程自动化把工程模板做到自动生成不仅能省五分钟还能避免手工替换时漏掉某个宏。4.2 哪些内容进模板哪些内容不进模板这是嵌入式模板化里最容易踩坑的问题。我的建议如下应该进模板的部分标准库源码但不要放在渲染流程里直接原样复制即可因为一般不变化所有以main.c、stm32f10x_it.c、system_stm32f10x.c为基础的源文件骨架预编译宏定义依据芯片和项目变量生成初始化代码里跟引脚、时钟相关的部分如GPIO_Config()、RCC_Config()项目说明README包含生成日期、芯片型号、烧录方式。不建议进模板的部分Keil的.uvprojx工程文件。这个文件虽然是XML文本但里面包含大量路径、UUID、调试器配置用模板渲染极易产生“生成后Keil打不开还必须手动重新配置”的问题。我的做法是保留一个基础工程文件作为静态模板只更新源文件目录不直接渲染它。ST固件库本体。标准库编译产物和源码都不应该通过模板生成直接拷贝即可否则版本容易混乱。4.3 一个嵌入式模板的变量设计我可以给出一个我在STM32F103C8T6项目里常用的一套变量变量名示例值作用project_namesmart_sensor_v2生成工程目录名、日志前缀mcu_typeSTM32F103C8T6宏定义、启动文件选择sysclk_hz72000000时钟树配置参数use_rtosfalse是否包含FreeRTOS相关初始化uart_enabletrue是否生成串口初始化代码authorzhangsan文件头注释然后写一个main.c的Jinja2模板片段#include stm32f10x.h {% if use_rtos %} #include FreeRTOS.h #include task.h {% endif %} void SystemClock_Config(void) { RCC_PLLConfig({{ sysclk_hz }}, ...); // 根据 mcu_type 选择FLASH延时配置 {% if mcu_type STM32F103C8T6 %} FLASH_SetLatency(FLASH_Latency_2); {% endif %} } {% if uart_enable %} void USART_Init(void) { // ... } {% endif %} int main(void) { SystemClock_Config(); // ... while (1) {} }脚本读取一个project.yaml配置文件渲染templates/下的所有.j2文件输出到output/{{ project_name }}/src/再把基础标准库文件复制过去最后生成一个Keil可打开的工程目录。整个过程跑一遍不到一秒钟一个可直接编译的C8T6标准库工程就诞生了。4.4 嵌入式场景里交互式还是配置文件开始我做过一版交互式命令行问“请输入项目名”“请输入目标芯片”但后来发现对团队不友好。原因很简单交互式的回答过程无法沉淀和审查一个项目是用什么参数生成的没有记录。后来改成读project.yaml然后把YAML文件和生成的工程一起入库别人拿到手一看就知道这个工程是怎么生成的。如果你想改参数改YAML重新执行即可。这个改动对嵌入式团队特别有用因为MCU型号、时钟参数这类关键配置一旦有历史记录排查问题会省很多时间。5. 实战场景二用IDEA模板把注释和格式化规范固定下来5.1 Live Templates本质上就是内建的代码生成器很多人把IDEA的Live Templates只当成“快速输入代码片段”的功能其实它比想象中强得多。它支持变量和函数表达式比如methodParameters()自动带出当前方法的形参列表methodReturnType()自动带出返回类型date()、time()自动带出当前日期时间user()自动带出系统用户名。这意味着它具备模板代码生成工具的核心能力固定格式 动态变量。团队里如果规范了方法注释、类头注释、版权信息完全可以靠Live Templates让每个成员“无脑插入规范代码”。一个典型的方法注释模板可以这样配置设置路径Settings - Editor - Live Templates新建一个组缩写填mc模板内容如下/** * $DESCRIPTION$ * * param $PARAMS$ * return $RETURN$ * author $USER$ * date $DATE$ */然后在Edit variables里DESCRIPTION普通变量默认无PARAMS表达式选择methodParameters()RETURN表达式选择methodReturnType()USER表达式选择user()DATE表达式选择date(yyyy/MM/dd)或date()。用的时候在方法上方输入mc按Tab就自动展开成规范的注释骨架。这个方法注释格式一旦统一再配合代码检查插件团队注释风格基本就锁死了。5.2 File and Code Templates新建文件的默认内容除了“插入片段”IDEA还支持控制新建文件的初始内容。在Settings - Editor - File and Code Templates里你可以修改Java Class的默认模板比如加上版权头/** * Copyright (c) 2025, Your Company. * All rights reserved. */ #if (${PACKAGE_NAME} ${PACKAGE_NAME} ! ) package ${PACKAGE_NAME}; #end /** * ${NAME} 是... * * author ${USER} * date ${DATE} */ public class ${NAME} { }只要在新文件对话框里输入类名这个模板就会自动生成。它本身就是一个非常实用的模板代码生成场景而且因为内嵌在IDE里几乎没有使用成本。很多团队把这份模板导出成配置新人装完IDEA一导入就自动拥有了团队规范。5.3 代码格式化模板怎么和生成器联动模板代码生成器能产出内容是一回事产出内容是否符合项目格式是另一回事。很多团队在IDEA里配置了一套Code Style比如缩进4空格、换行宽度120、大括号换行风格但生成器脚本生成的代码却是另一套风格导致每次生成完都要手动格式化。我的建议是把IDEA的Code Style方案导出为一个config.xml提交到仓库同时让生成器脚本在输出后自动调用一次格式化命令。比如Java后端可以用google-java-format前端可以用prettier --write。一句话生成器输出 格式化钩子 一次产出规范代码。这个组合比“生成后再让IDE整理格式”要可靠得多。5.4 测试用例模板和测试报告模板同理别以为只有业务代码能用模板。测试用例模板、测试报告模板、接口文档模板本质上都是模板代码生成。我自己测试用例写得最多的场景是在IDEA里配好一个JUnit测试类的Live Templates包括Test、测试方法命名、Mock、断言再用一个脚本把接口清单Excel渲染成接口测试报告Markdown。这套组合让我在写测试时减少了很多机械劳动。你如果平时还要跟测试用例模板、测试报告模板打交道完全可以套用上面这些章节的思路固定框架写进模板用例数据从表格里读渲染成最终文档。6. 实战场景三用Excel模板驱动数据治理项目和后台管理系统的生成6.1 数据治理项目为什么总离不开“Excel模板导入”我在做数据治理类项目时发现一个高频需求业务人员/数据管理员习惯用Excel维护元数据、数据字典、数据质量规则而研发侧的数据库表和接口需要这些信息。如果靠人手工从Excel抄成SQL再写代码既慢又容易出错。所以这类项目常规做法是先定一套Excel模板明确列结构比如字段名、字段类型、长度、是否主键、是否可空、默认值、字段注释、业务域。业务人员填Excel系统负责导入解析。数据治理平台应有的核心功能一般包括模板配置指定Excel模板的Sheet、列映射、必填校验模板下载与示例给业务方一个带示例数据的模板导入解析与校验校验字段名是否重复、类型是否合法、主键是否重复变更版本管理记录每次导入的版本差异支持回滚元数据血缘从Excel/数据库到代码、报表的血缘追踪质量规则管理把Excel里的非空、唯一、长度规则转化为SQL校验规则。从“模板代码生成工具”的角度看所有从Excel模板抽取数据再输出SQL、Java代码、前端页面的环节都算一种模板生成。6.2 把Excel表格当成生成器的上下文数据这里我分享一个最小实现思路用openpyxl或pandas读取Excel把每一行转成字典再交给Jinja2渲染。比如有一张user_table.xlsx内容是字段定义字段名类型长度主键可空注释idBIGINT20是否主键IDnameVARCHAR64否否用户名emailVARCHAR128否是邮箱生成脚本里先把数据转换成一个Python listfields [ {name: id, type: Long, primary: True, comment: 主键ID}, {name: name, type: String, primary: False, comment: 用户名}, {name: email, type: String, primary: False, comment: 邮箱}, ]然后渲染一个Java实体类模板逻辑和前面3.2节一样只是数据来源从Excel读取而不是手写死。再把同一个fields传给前端列表页模板就能同时生成Table列定义和表单字段。这就是“一份Excel模板多处生成”的典型用法。6.3 后台管理系统模板如何与代码生成器联动后台管理系统是模板化程度最高的领域之一。现在比较通用的做法是先有基础模板登录、权限、菜单、用户管理、角色管理、日志审计这些属于固定文本业务模块订单管理、客户管理、商品管理则属于变化部分可以用代码生成器根据数据模型批量生成。我的实际套路是这样定义业务模型Excel模板包含表名、实体名、字段列表后端用Jinja2生成Controller、Service、Mapper、Entity前端生成List.vue、Form.vue、API.js同时生成菜单SQL插入到权限表最后执行格式化脚本和编译测试。这样几个操作连起来一个新业务模块的后台管理功能从数据模型到可联调页面通常能控制在分钟级。整个流程里“后台管理系统模板”是基础而模板代码生成器负责把重复的增删改查页面自动化。缺了生成器系统模板只是一套静态代码价值会大打折扣。6.4 这类场景最需要注意的源数据和生成结果的版本漂移Excel作为数据源有个天然问题它很难做严格的版本管理。今天张三改了一版字段明天李四又改了一版数据库表结构可能已经变了但Excel里没同步。解决这个问题我一般会做三件事Excel模板里加一列“变更说明”每次导入必须填写生成时先对比数据库当前结构和导入的元数据生成差异报告而不是直接覆盖生成的代码不做为唯一真源保留一份结构变更历史到版本仓库比如把读取到的结构导成JSON/YAML提交。只要版本漂移能被发现后面出问题就有迹可循。这也算是在数据治理场景里反复被验证的经验。7. 模板代码生成工具用久了的几个真坑7.1 转义问题模板引擎和生成目标语言“抢符号”这是所有模板使用者第一个遇到的大坑。Jinja2用{{ }}标记变量但如果你要生成的是Vue组件或Go template目标文件本身也存在{{ }}。渲染时模板引擎会尝试解析它们然后报错或输出错误内容。解决办法有几种在Jinja2里用{% raw %}...{% endraw %}包住目标文件中原生的模板语法或者把引擎改成其他定界符比如${}或者在目标文件里写{{ { }}这种转义形式看引擎支持程度。生成XML/JSON时特别注意特殊字符。比如要生成一段包含的XML必须确认模板引擎是否自动转义如果自动转义了反而可能导致生成的内容和预期不符。建议生成后立刻抽查几个关键位置别等到编译期报错。7.2 编码与BOM问题Windows和Linux打架代码生成工具一旦团队混用Windows和Mac/Linux文件编码就是大麻烦。Windows下一些编辑器默认用GBK/ANSILinux下默认UTF-8。如果模板文件本身是UTF-8生成器脚本在Windows下运行可能把输出写成GBK导致Keil或Java编译器出现中文乱码。我的统一做法是所有模板文件、数据文件、输出文件一律UTF-8无BOM。Python脚本里打开文件时显式指定encodingutf-8写文件时也明确指定。如果目标环境确实需要GBK比如某些老旧的Keil工程那就把“编码转换”做成一个显式步骤而不是靠环境默认值糊弄。7.3 覆盖风险生成器把手工改动冲掉了模板代码生成工具最大的风险就是“二次生成时覆盖手工改动”。第一次生成后你往往会在生成结果上做一点定制——改个方法名、加一段特殊逻辑。然后某天你修改Excel模板重新执行生成刚才的手工改动可能被全部覆盖。针对这个问题我现在有两个习惯所有生成器强制支持--dry-run参数只打印将会产生哪些文件、哪些文件已有且会被覆盖不真正写入核心生成目录纳入Git管理二次生成前先git diff看差异生成后逐项review diff再提交。另外尽量让模板“可再生成”。换句话说把需要手工定制的地方也抽象成变量或者单独的扩展点而不是直接在生成结果上改。如果实在没法避免手工改动那就把这些文件加入生成器的“跳过列表”明确告诉它不要覆盖。7.4 不要为了模板化而模板化还有一个反向的坑过度模板化。有人会把整个工程的所有文件都塞进模板连编译配置文件、IDE配置、第三方库源码都试图生成。结果模板里全是转义和条件分支维护成本比直接复制还高。我的经验是识别“值得模板化”的标准高频重复这个文件是不是每次新项目都要改高一致性要求这个文件是不是一旦格式不一致后面就很容易出问题文本内容为主二进制文件图片、编译产物不要碰具备稳定结构这个文件的结构是不是已经收敛如果还在频繁调整先别模板化。像Keil的.uvprojx、IDE的.idea目录这类“一次生成后很少动”的文件我一般保留静态版本不进模板。你不是要把整个世界都抽象成模板你只是想把重复劳动交给机器。7.5 其他领域的模板底层逻辑都一样写到这里再回看那些五花八门的热搜词——Latex期刊模板、论文模板、PPT模板、AE模板、文生视频提示词模板、测试报告模板、Excel导入模板——它们本质上都没有逃出模板代码生成工具的框架固定外壳 变化变量 渲染规则。你如果理解了代码侧的模板生成逻辑再去看Latex投稿模板会发现那就是“一个模板引擎渲染出PDF源的流程”你去设计一份测试用例模板也会自然地考虑哪些字段是固定列、哪些是每次填写的变量。用处最大的反而是这套思维方式拿到任何重复性文档或代码任务先想“哪些是不变的哪些是变的”然后用一个最简单的脚本把它们渲染出来。这样一来模板代码生成工具就不再是某个特定软件而是你随时可以拿起的思维工具。我自己用下来最大的体会是模板代码生成工具并不是要把所有东西都“生成”出来而是把那些重复、易错、有规范的部分自动化把时间留给真正需要判断力的事情。如果你也想上手别一开始就研究那些重型代码生成平台先把自己最近复制过三次以上的文件列出来把不同的地方抽象成变量写一个几十行的渲染脚本跑通。等这个最小区块稳定之后再考虑做成团队可用的脚手架。最后提醒一点把模板也纳入版本管理按项目维护好参数配置文件。你会发现“改模板”往往比“改代码”高效太多也安全太多。