先说我自己的一个判断:代码生成这个事,听起来好像是大厂才有的基建能力,但实际上只要你写过三五个重复度极高的CRUD接口,或者手工敲过上百行格式固定的配置文件,大概率都动过“自己搞一个生成器”的念头。行业里聊代码自动生成框架,其实分了两条完全不同的路线:一条是规则模板驱动,靠元数据和模板引擎机械地产出代码,典型代表就是若依这类后台管理框架里的代码生成器;另一条是AI大模型驱动,靠自然语言描述直接生成代码,或者用Agent框架做自动补全和重构。这几年聊生成式AI的人多,但真正在公司里落地跑得稳的,反而是第一类——它简单、高效、不烧token,产出的代码完全可控。这篇博客我打算把自己从零搭建代码生成框架的完整过程讲清楚,包括框架分层、自定义规则的实现、嵌入式场景下Simulink模型到C代码的生成链路,以及当前最热的AI辅助生成方向选型,给正在调研或者准备动手的读者一个可以借鉴的样本。
代码生成框架能解决什么问题,说白了就是一句话:把“重复且有规律”的脑力劳动,变成“配置加渲染”的机械劳动。比如后端接口的Controller、Service、Mapper三件套,本质上就是表结构到代码的一次映射;再比如嵌入式开发里根据Simulink模型生成C代码,核心也是把模型表达的逻辑翻译成目标语言。这些话听起来简单,但真正动手之后才会发现,难点不在“生成”这两个字,而在“框架”这两个字——怎么把变化的部分隔离出来、怎么让规则可自定义、怎么保证生成的代码能直接进CI而不是当一次性废料,这些才是决定一个生成工具是玩具还是生产力的分水岭。
1. 先把“代码生成框架”拆开看:两种路线与适用边界
1.1 规则模板型与AI生成型:各自解决什么问题
很多人搜“代码生成框架”这个词,其实想找的东西并不一样。我把市面上能见到的工具粗略分成三类,方便大家对照:
| 类型 | 代表思路 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 规则模板型 | 若依代码生成器、自研模板引擎 | 结果确定、可测试、离线运行、不烧token | 灵活度低,只能生成固定模式 | CRUD、DTO、Mapper、配置文件、测试骨架 |
| AI生成型 | 大模型代码补全、Copilot、AI代码生成工具 | 理解自然语言,通用性强 | 结果不确定、上下文受限、token成本 | 复杂逻辑编写、算法原型、SQL/正则表达式生成 |
| 混合型 | 规则定骨架 + AI填业务 + 人工审核 | 可控性和灵活性兼顾 | 架构复杂度高,需要工作流编排 | 中大型项目的代码生成平台 |
规则模板型之所以在工程界活得很好,核心原因是产出确定。你给它一张表结构信息,它出来的Controller是什么样就是什么样,版本之间的差异可以用diff工具看得清清楚楚。这对团队协作、代码Review、质量回溯都有巨大的好处。AI生成型更多被当成“结对编程助手”,适合在思路不清楚、写法不常见的时候救场,但真要把它当成框架的底层引擎,是需要很强的工程兜底能力的。
1.2 什么样的代码适合自动生成
这是我踩过坑之后才总结出来的一句话:代码生成框架适合解决“重复且有规律”的产出,不适合解决“研发逻辑本身”。适合的典型场景我列一下:
- 后端接口的CRUD层:实体类、Mapper接口、Mapper XML、Service接口、ServiceImpl、Controller。字段基本由数据表结构决定,几乎没有业务分支。
- 配置类代码:Spring的Bean配置、K8s的YAML、数据库初始化脚本、DTO/VO/Pojo转换代码。
- 客户端与服务端的数据模型:根据接口定义文档生成Swift/Kotlin/TypeScript模型类。
- 测试骨架:pytest的测试类、Java接口自动化测试框架里的用例模板。
- 嵌入式模型代码:Simulink模型生成的C代码、状态机生成的C代码。
不适合自动生成的部分,主要是业务流程状态机、算法核心逻辑、涉及多表事务的复杂操作。这些代码的表象并不由结构决定,而是由业务规则决定,规则变了代码就变,硬套模板只会让代码变成一坨没法维护的“生成物”——比手写的还难改。我见过一个团队试图用模板生成多表联查的复杂Service方法,结果模板里写了一堆条件判断和循环,比手写代码复杂三倍,最后那个模板改一次要半天,没人愿意碰。
1.3 框架分层的通用思路
不管用哪种路线,一个正经的代码生成框架,我都会按四层去设计:
- 数据接入层:负责读取元数据。后端场景直接连数据库读表结构,嵌入式场景读Simulink模型或接口描述文件,前端场景读OpenAPI/Swagger文档。
- 规则配置层:定义生成行为的全部开关和参数。哪个表生成哪几层代码、包名叫什么、类型映射规则是什么、要不要生成分页接口等,全部收敛到配置里。
- 模板渲染层:真正的“代码工厂”。用Freemarker、Jinja2或Velocity这类模板引擎,把元数据和规则配置渲染成目标代码文本。
- 输出与后处理层:负责文件落盘、目录结构创建、格式化、版权头补齐,再理想一点,直接跑一下编译或者lint检查,保证产出是能进CI的。
这四层各管各的事,互不掺和。最关键的是第二层和第三层之间的边界——规则配置文件里只写字面参数,模板里只写渲染逻辑,两者通过一个统一的上下文对象交互。我之前见过有人把业务逻辑直接写在模板里,那个模板后来没人能改得动,等于废了。实际上很多自研的代码生成器最终变成“一次性工具”,就是因为没守住这个边界,把变化和不变的东西揉在一起,后面想扩展就只能推翻重来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 规则模板引擎实战:如何自定义一套生成规则
2.1 元数据从哪来:数据源解析与表结构提取
我现在做后端代码生成,第一步永远是连数据库拿表结构。这一步绕不开JDBC的DatabaseMetaData接口,它能帮你拿到表名、字段名、字段类型、是否主键、是否可空、字段注释等几乎全部需要的信息。核心逻辑大概长这样:
java复制DatabaseMetaData metaData = connection.getMetaData();
ResultSet tables = metaData.getTables(null, null, "%", new String[]{"TABLE"});
while (tables.next()) {
String tableName = tables.getString("TABLE_NAME");
String comment = tables.getString("REMARKS");
ResultSet columns = metaData.getColumns(null, null, tableName, "%");
while (columns.next()) {
String columnName = columns.getString("COLUMN_NAME");
String dbType = columns.getString("TYPE_NAME");
int size = columns.getInt("COLUMN_SIZE");
boolean nullable = columns.getInt("NULLABLE") == DatabaseMetaData.columnNullable;
String columnComment = columns.getString("REMARKS");
// 组装成统一的 ColumnMeta 对象
}
}
注意几个容易踩的细节。MySQL里REMARKS能拿到表注释,但有些数据库驱动对注释的支持时好时坏,最好在封装层做兜底,拿不到就给个空字符串。另外主键信息需要另外查getPrimaryKeys,不包含在getColumns的结果里。还有大小写问题,Oracle的表名默认大写,生成类名时要统一做大小写归一化。
拿到这些元数据之后,我会把它转换成平台无关的中间模型。表的元数据对应TableMeta,字段对应ColumnMeta,中间模型不再带任何数据库专有类型,全部是标准字符串和枚举,这样后续模板渲染时不会因为数据库差异而写一堆条件分支。这一步就是所谓的“模型抽象”,是后面模板保持简洁的前提。
2.2 模板引擎选型与Freemarker落地
模板引擎的选择,基本上看技术栈。Java后端我首选Freemarker,因为它的语法足够简单,团队上手快,而且对null值处理比较宽容;Python系用Jinja2是标配;前端代码生成现在也有不少人用Nunjucks。不推荐为了“功能全”去上太重的模板引擎,代码生成模板真正用到的语法大概只有if、list、取值这三种,其他都属于锦上添花,学多了反而没人维护。
用Freemarker写一个Controller模板,核心就两段:包名引入和类体定义。
freemarker复制package ${basePackage}.controller;
import ${basePackage}.service.${entityName}Service;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("/${entityVarName}")
public class ${entityName}Controller {
@Resource
private ${entityName}Service ${entityVarName}Service;
@GetMapping("/{id}")
public Result<${entityName}VO> getById(@PathVariable Long id) {
return Result.success(${entityVarName}Service.getById(id));
}
@PostMapping
public Result<Boolean> create(@RequestBody ${entityName}DTO dto) {
return Result.success(${entityVarName}Service.create(dto));
}
}
模板里的${entityName}、${entityVarName}这些变量,就是规则配置层传进来的上下文。在实际项目里,我会把需要的变量全部塞进一个Map,命名规则固定,比如entityName代表实体类名、entityVarName代表首字母小写的实体变量名、basePackage代表基础包名。这个Map就是模板和规则之间的协议,宁可多塞几个不用的变量,也不要让模板自己去读数据库或做字符串加工。
2.3 核心规则配置:类型映射、命名转换与分组输出
自定义规则是这类框架的灵魂。我会用一份YAML来描述生成行为,模板尽量保持“愚蠢”,规则全部看向配置。
yaml复制generator:
basePackage: com.example.demo
typeMapping:
varchar: String
bigint: Long
int: Integer
decimal: BigDecimal
datetime: Date
tinyint: Boolean
naming:
tableToClass: upperCamelCase
columnToField: lowerCamelCase
outputs:
- layer: controller
enabled: true
template: controller.ftl
suffix: Controller.java
- layer: service
enabled: true
template: service.ftl
suffix: Service.java
- layer: mapper
enabled: true
template: mapper.ftl
suffix: Mapper.java
类型映射是生成质量的重灾区。数据库的tinyint(1)在MySQL里经常被当成Boolean,但tinyint(2)又该是Integer;DATE、TIME、DATETIME、TIMESTAMP虽然都是时间类型,但在Java里的处理方式完全不同。我的建议是不要写死映射表,把它放到规则配置里,让不同项目自己维护。每个项目可能有自己的规范,比如有的公司要求decimal一律映射BigDecimal,不允许用Double。这个看起来不起眼的决策,能直接影响生成代码能不能通过团队的代码评审。
命名转换同样如此。表名sys_user要转成SysUser,字段名user_name要转成userName,大多数情况下一个下划线转驼峰的正则就够用了。但真有那种“数据库里字段名是纯大写缩写”的老系统,这时候库名、表名、字段名的归一化规则就得单独维护。我一般会把命名转换器做成策略接口,默认提供下划线转驼峰和保持原样两种,特殊项目再加自定义实现,而不是在模板里处理命名逻辑。
2.4 一套可维护的规则仓库设计
这里说的“规则仓库”不是代码仓库,而是指把所有生成规则、模板、产物样本集中管理的机制。我见过很多团队把模板散落在各个项目里,生成逻辑也复制了无数份,最后没一个人说得清楚“线上代码到底是怎么来的”。我的做法是:
- 模板文件单独建一个目录,按输出层分文件夹:controller、service、mapper、xml。
- 每个模板旁边放一个example输出,作为生成结果的“黄金样本”。模板改动后,用黄金样本做diff,差异一眼就能看出来。
- 规则配置按项目隔离,公共规则抽到公共配置里,项目级规则只保留差异部分。
- 生成结果强制跑一次编译和代码格式化。编译失败就报错,不允许生成“半成品”。
这套机制看着笨,但真正能保证一个团队在用生成框架半年之后,依然敢把生成的代码合进主干。没有这套约束,生成框架的代码很快就会腐化成谁都不敢碰的怪兽。黄金样本特别重要,它相当于给模板加了回归测试,任何模板改动导致输出结构变化,都能第一时间暴露出来。
3. 嵌入式场景:Simulink模型到C代码的生成链路
3.1 模型驱动开发到底在解决什么问题
聊完规则模板这一路,再把目光转向嵌入式领域。在这个圈子里,“代码自动生成框架”有另一层含义,那就是基于Simulink模型的C代码生成。传统嵌入式开发是画流程图、写文档、手敲C代码,模型驱动开发是直接在Simulink里把控制算法搭出来,然后让工具链自动生成可编译的C代码。
这样做的好处非常明显:算法行为可以在模型层面做仿真验证,比直接写C代码调试早发现问题;同时模型作为“单一事实来源”,文档和代码都从它派生,不会出现代码和设计文档对不上的情况。但代价是,工具链和生成配置的学习成本不低,不是装个软件点个按钮就能跑通的。
3.2 生成配置中必须拿捏的几个参数
想在Simulink里把C代码顺利生成出来,关键不在于建模本身,而在于生成配置。我在实际项目中反复确认过几个重要参数:
- 系统目标文件(System Target File):如果是做产品级嵌入式代码,通常选ert.tlc(Embedded Real-Time target),它生成的是没有操作系统的裸机代码,可移植性好;做快速原型就用grt.tlc。选错目标文件,生成代码的形态会差很多。
- 求解器类型:如果模型里全是连续时间积分器,选固定步长离散求解器;如果模型只做逻辑判断和状态切换,那步长和求解器对代码影响不大,但配置也不能乱来。
- 代码生成语言和文件打包方式:选择C语言,文件可以按模块拆分成多个.c/.h,也可以全部打到一个文件里。拆分后方便阅读和单模块替换,但文件数量多;合并后集成简单,但可读性差。
- 函数包装方式:每个子系统要不要生成单独的C函数,产出函数名怎么映射到模块名。这个可以直接影响上层调用方对生成代码的调用方式。
这些参数不会在默认配置里给到你最优解,需要对照目标MCU的编译器和集成方式去试。我的建议是,第一次配置花半天时间逐项读一遍文档,把模型和生成代码的对应关系记清楚,后面能省很多事。我曾经因为漏看了一个“函数名与模块名映射”的选项,结果生成的函数叫Bounce_Init、Bounce_Step,和工程里其他模块的函数风格完全不一致,最后只能靠脚本批量改名来收场。
3.3 生成代码与手写工程的集成方式
配置再完美,生成的C代码也总要和已有的MCU工程集成。这里有一层常见的认知差:很多人以为生成代码是直接拿来替换手写代码,实际上主流做法是做“接口隔离”。生成的模块对外暴露的接口与内部实现解耦,手写代码通过接口调用它,两者之间不互相嵌套。
我常用的集成流程是:
- 在模型里为每个子系统定义好输入输出端口,生成代码时让每个子系统生成一个独立的C文件。
- 在MCU工程里新增一个models目录,专门放生成代码,和手写代码物理隔离。
- 生成代码只由工具刷新,不允许手改。如果确实需要调整逻辑,回到模型改,再重新生成,保证“模型是唯一真相”。
- 手写代码通过头文件调用生成代码的接口,头文件里的接口命名在模型里提前定义好,而不是生成后靠脚本去替换。
这套流程跑顺之后,最大的收获是“改逻辑”这件事变得很快。算法参数要调,去模型里改个常数重新生成,替换对应文件就行。不用再像以前那样在一堆C文件里人肉搜索魔改常量,改完还要提心吊胆怕影响其他逻辑。
4. AI辅助路线:大模型、RAG与Agent框架的正确打开方式
4.1 什么场景该用AI生成而不是模板
规则模板引擎能覆盖很大一部分重复代码,但剩下的部分怎么办?复杂业务表达式、结构不固定的胶水代码、正则表达式,这些东西用模板写规则,规则本身会比代码还复杂。我目前看到比较务实的方案是:在模板引擎的旁边,接一个大模型生成通道。
举个例子,根据字段元数据生成一个数据校验规则。如果是JSR 303注解,模板完全能搞定——表字段不能为空就加@NotNull,有长度限制就加@Length。但如果你想生成的是复杂跨字段校验方法,比如“结束时间不能早于开始时间,且状态为终态时不参与计算”,这种情况靠模板字段映射就是天方夜谭,需要AI根据语义理解来生成。再比如生成一个单元测试的边界用例,模板只能生成空壳,AI能根据函数签名和上下文补充有意义的断言。
4.2 RAG把项目上下文装进提示词
AI生成代码最容易翻车的地方,不是模型能力不够,而是上下文太窄。模型不知道你们项目用没用Lombok、不知道实体类喜欢用哪种风格、更不知道数据库命名规范是下划线还是大写。所以我不建议直接拿着裸的提示词让大模型生成代码,更稳的方案是走RAG(检索增强生成)。
RAG在代码生成里的落地形态,和我给文字知识库做问答是同一套思路。离线阶段把项目里已有的优秀代码片段、编码规范文档、常用工具类用法向量化存起来;在线阶段收到生成请求时,先把请求里的关键词转成向量,从库里检索最相关的代码片段和规范片段,拼进提示词再发给大模型。
伪代码大概是这个样子:
python复制def generate_with_rag(request: CodeGenRequest) -> str:
context = vector_store.search(request.keywords, top_k=5)
prompt = build_prompt(
system="你是本项目的高级开发工程师,请严格遵循本项目的代码规范。",
example_codes=context.examples,
requirements=request.description,
schema=request.table_schema
)
return llm.chat(prompt)
这里有个很关键的细节:RAG检索回来的“示例代码”权重要高。如果你把项目里一个写得最规范的Service实现类作为示例放进提示词,大模型产出的风格会和项目风格非常接近,比在提示词里写一百遍“请遵循开发规范”都管用。我实测下来,带示例的生成结果在做代码评审时,被挑出的风格问题能少一半以上。这个结论其实也很好理解,大模型在代码风格上的模仿能力远强于规则遵循能力,给它一个具体样本,比给它一堆抽象要求有效得多。
4.3 Agent工作流:规划、执行、校验三步闭环
RAG解决的是生成内容的质量,Agent工作流解决的是生成过程的自动化程度。现在的Agent框架五花八门,但落到代码生成这个具体场景,我认为核心流程就三步:规划、执行、校验。
第一步规划,Agent根据用户需求拆解出要生成哪些文件、每个文件的职责和依赖关系。第二步执行,逐个文件调用大模型生成,或者调用模板引擎生成,写完一个文件之后自己读一遍检查是否有明显的引用缺失。第三步校验,编译或语法检查,发现问题就带着报错信息回到执行阶段,让模型自己修。这三步循环直到编译通过或者达到最大迭代次数,再把最终结果交给开发者做人工Review。
在实现上不需要一上来就引入特别重的Agent框架。我自己踩过一遍之后发现,直接用代码写一个简单的状态机就能跑通这个闭环,核心伪代码如下:
python复制def run_agent(request):
plan = llm_plan(request) # 规划:生成文件清单和依赖关系
for file_spec in plan.files:
code = generate_file(file_spec) # 执行:模板或大模型生成
errors = run_check(code) # 校验:编译、lint、格式
for i in range(3): # 最多自修复三轮
if not errors:
break
code = llm_fix(code, errors)
errors = run_check(code)
write_file(file_spec.path, code)
只有当生成或者自修复连续多轮都失败的时候,才把半成品和人遇到的问题一起提交给开发者。这个“人机协作”的边界非常重要,一旦Agent在特定步骤连续失败,让它无限重试只会越改越乱,不如早点叫人。我见过一个Agent脚本因为循环调用大模型修复编译错误,最后生成了十几轮完全不同的代码,浪费了一堆token不说,输出质量也没见提升。
4.4 规则与AI结合的推荐形态
到这一步,我觉得一个相对成熟的“代码自动生成框架”,应该是双层结构。底层是规则模板引擎,负责所有结构固定的产出,稳定、快、零成本;上层是AI服务,负责结构不固定、需要语义理解的产出,用一个工作流编排模块把两者串起来。
比如生成一个新模块,流程可以是:先用规则引擎把CRUD五层全部生出来,再用AI服务根据业务描述生成核心的业务Service方法体,最后人工Review并补充边界条件。这样既保证了大盘的代码质量稳定,又解决了模板覆盖不到的个性化问题,token消耗也控制在一个合理范围,整个生成链路处于一个可监控、可复盘的状态。
5. 排错实录与避坑指南
5.1 高频问题速查表
做代码生成框架久了,踩过的坑基本都能整理成一张速查表。这里分享几个我遇到的实际问题,按频率排序:
| 问题现象 | 原因 | 解决方案 |
|---|---|---|
| 生成的Java文件中文注释乱码 | 模板文件和输出文件的编码不一致 | 统一模板文件保存为UTF-8,在模板引擎配置里强制outputEncoding=UTF-8 |
| 类型映射错误,tinyint被生成成Integer而不是Boolean | 只按类型名匹配,没关注显示宽度和业务语义 | 在YAML里按类型名加精度维度定制映射,必要时提供回调函数让规则可编程 |
| 表名是复数或带前缀,生成类名不符合约定 | 命名转换规则只做了下划线转驼峰 | 在规则层增加前缀剥离配置,比如统一去掉t_前缀再转驼峰 |
| 生成文件的包名和目录不一致 | 模板里包名写死,没跟随basePackage | 模板里所有包名一律从上下文取,禁止在模板中出现硬编码 |
| 重复生成时旧文件残留 | 输出层只写新增文件,没做清理 | 每次生成前记录文件清单,执行时对目录做同步,删除不在清单中的旧文件 |
| 生成的代码与手写代码命名冲突 | 没有做命名空间隔离 | 在后处理层增加冲突检测,发现重名文件时打印警告并拒绝覆盖 |
5.2 排查思路与调试技巧
生成框架出问题,最怕的就是黑盒。为了让自己不被“为什么生成结果不是我想要的”这个问题反复折磨,我做了三件事:
第一,保留元数据快照。每次生成时,把输入的表结构信息、规则配置快照成JSON存档,出问题时先看快照,确认输入没有变化再往下排查。很多“奇怪的问题”其实是数据库表结构被人改过,但通过快照一眼就能发现,不用去数据库里翻历史记录。
第二,模板单独调试。Freemarker模板可以直接写一个main方法,用测试数据渲染,把渲染结果打到控制台,和黄金样本做对比。这样比每次跑完整生成流程再去翻日志快得多。我甚至会把模板调试做成一个独立的CLI命令,方便在任何环境里快速复现渲染结果。
第三,加一个“生成痕迹”日志。每个生成文件都在末尾追加一行注释,说明这条代码是由哪个模板、哪次配置生成的。这个信息在后续追查线上问题时会非常有用,相当于代码的“生产履历”。比如线上出了一个诡异的bug,打开文件看到注释,就能直接定位到这是某个模板版本生成的,快速判断要不要回退或者重新生成。
5.3 代码生成框架的工程化管理
最后再聊一点工程化层面的经验。代码生成框架本身也是一段代码,也需要版本管理、测试和评审,否则它就会成为团队里最危险的那个“一次性脚本”。
我的建议是把生成框架当成一个正式的基础组件来管理。模板的改动要走Merge Request,配上黄金样本的diff结果再做评审;规则配置的默认值要经过测试用例约束,不能谁顺手就改了全局默认;生成的代码要纳入CI的编译检查,不能允许“生成一个模块到一个新工程里,一编译全红”这种情况发生。
在安全上也要留一道口子。模板引擎如果支持任意表达式,就得小心注入问题。比如Freemarker有?new和内建函数,如果规则配置来源不可信,模板注入是可以搞出文件读取甚至命令执行的。在内部使用还好,一旦要开放给公司外部或者做成平台,必须对模板做白名单校验,禁用危险的内建函数。这个点很多做内部工具的人会忽略,因为默认信任使用方,但一旦工具开放给更多人使用,风险就会被放大。
做了这么久代码生成相关的工作,我个人最深的一个体会是:代码自动生成框架的难点从来不在“自动”两个字,而在“抽象”两个字。你得能从一堆业务代码里抽取出稳定不变的骨架,并且识别出哪些地方是允许变化的接缝;没有这个抽象能力,再强的模板引擎和大模型都救不了你,因为生成出来的代码大概率是“形似而神不似”,看着能编译过,真要迭代就会发现处处是坑。我自己到了一个新项目,会先花几天有意识地做“重复代码考古”,把相同模式的代码按维度归类,然后再决定要不要搭生成框架、搭到什么程度。很多团队一上来就追求一步到位的全自动生成平台,结果项目没跑起来,反而背上了一个沉重的框架负担。如果你也想搞这么一套东西,我的建议是从小处切入,先选一个重复度最高的场景跑通闭环,再逐步扩大范围。等你手里的规则、模板和AI通道都经过实战打磨之后,代码生成框架才会真正成为团队效率的放大器,而不是一个需要你持续供奉的玩具。
