前几年我还在靠复制粘贴写CRUD的时候,最大的愿望就是有个东西能替我把那堆重复的增删改查代码一口气全写出来。后来真正深入去研究代码生成器,才发现这玩意儿并没有那么玄乎,核心就是三板斧:摸清楚业务表的元数据、找一套顺手的模板引擎、再把生成规则沉淀成配置。今天这篇就从一个实际开发者的角度,把代码生成器从设计到落地的完整链路掰开揉碎讲清楚。
先说我做这个东西的背景。团队后端技术栈是Spring Boot + MyBatis-Plus,前端是Vue3 + Element Plus,每次接一个新模块,光写entity、mapper、service、controller、vue页面这些基础代码就要磨蹭一两天,而且出错率极高,字段名手误、类型对不上、关联查询漏条件,全是细节问题。后来我决心基于若依框架的代码生成思路,自研一套适配团队业务的代码生成器,从设计原理、模板编写到实际改造,整个过程跑通之后,新模块的开发效率至少提升了50%,我现在把完整经验分享出来。
1. 代码生成器的设计原理与整体架构
1.1 为什么需要自研代码生成器
市面上现成的生成工具不少,最典型的就是若依(RuoYi)自带的代码生成模块,此外还有MyBatis Generator、MyBatis-Plus Generator等。说实话,直接用这些工具确实能解决大部分问题,但落到真实业务里,总能碰到一些别扭的地方:
- 模板是写死的,如果想在生成的entity里自动加一段创建人、更新人的审计字段注释,改起来费劲。
- 生成风格和团队规范未必一致。比如我们团队要求所有service接口都必须有@Transactional注解标记,但默认模板里并没有,每次生成完还得手动补。
- 字段类型映射规则单一。比如数据库里有个
deleted字段,我们希望它自动映射成Boolean类型且加@TableLogic注解,这个默认模板做不到。
自研的成本其实没有想象中高,核心就两件事:读取表结构的元数据 + 把元数据塞进一套可维护的模板里。搞清楚了这两件事,后续想加什么花活都是自由发挥。
注意:自研不代表推翻重来,恰恰相反,我建议先深入研究若依这类成熟框架的生成逻辑,把优秀的设计思路借鉴过来,再针对自己的项目骨架做定制。站在巨人的肩膀上改进,永远比从零造轮子靠谱。
1.2 整体架构分层设计
我设计的代码生成器分为四层,每层职责非常清晰:
| 层级 | 核心职责 | 关键组件 | 具体说明 |
|---|---|---|---|
| 数据源层 | 读取数据库表结构 | JDBC连接、DatabaseMetaData | 获取表名、字段名、字段类型、注释、主键信息 |
| 元数据层 | 将原始信息转换为可用的Java模型 | TableInfo、ColumnInfo | 屏蔽不同数据库的差异,统一字段命名和类型映射 |
| 模板引擎层 | 根据模板生成代码文件 | Velocity / FreeMarker / Thymeleaf | 决定输出格式和内容结构 |
| 输出层 | 管理文件生成策略 | 覆盖策略、目录映射、格式化处理 | 将生成的代码写入目标路径 |
只要把数据源层的信息取准了,后面三层都是确定性工作。说白了,代码生成器就是一个管道:数据库表结构进去,一套完整代码出来。任何一个环节的精准度不够,最终生成的代码质量都会打折扣,所以每一层我都有针对性的设计。
1.3 基于若依思路的扩展设计
若依代码生成器其实是这个领域的一个标杆实现。它的思路很值得学习:配置数据源 -> 导入数据表 -> 编辑字段配置(列表显示、表单类型、查询方式等) -> 选择模板生成 -> 下载代码或直接生成到项目目录。
我在自研时保留了这套核心流程,但做了三处关键扩展:
- 配置信息入库。若依把每张表生成的配置存到
gen_table和gen_table_column两张表里,我同样设计了两张配置表,这样二次调整字段配置时不用反复读取数据库,而且支持同一张表配置多套生成方案(比如PC端和移动端各一套)。 - 模板热加载。我把模板文件放在一个独立的
template目录下,修改模板后不用重启服务,定时扫描检测到文件变更就自动重新加载。这个功能在调试模板时非常省事,配一套模板调一个星期是常态。 - 远程生成和本地生成双模式。团队开发时用远程模式把代码生成到Git仓库统一的目录,个人调试时用本地模式输出到临时文件夹,互不干扰。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心模块的选型分析与技术决策
2.1 模板引擎的对比与选择
这是整个代码生成器最核心的选型决策。模板引擎本质上解决的是“把元数据和文本模板混合渲染”这个问题,市面主流选项有FreeMarker、Velocity和Thymeleaf,各有优缺点:
| 模板引擎 | 性能表现 | 语法友好度 | 中文社区活跃度 | 特点 |
|---|---|---|---|---|
| FreeMarker | 高 | 中,宏功能强大 | 高 | 功能全面,适合复杂模板,循环/判断/宏定义非常灵活 |
| Velocity | 较高 | 高,语法简单 | 中(处于维护状态) | 上手极易,模板写起来直观,但近年更新缓慢 |
| Thymeleaf | 中 | 中(面向HTML场景更优) | 中 | 主要用于Web视图渲染,在纯代码生成场景优势不明显 |
我最终选择了FreeMarker。原因有三:一是它的宏定义能力非常强,可以把“字段列表展示”这类重复段落封装成宏,多个模板复用;二是官方文档完善,遇到语法问题基本一搜就有答案;三是若依用的就是Velocity,但我在改造中觉得FreeMarker的类型转换和空值处理更严谨,更能适应复杂模板场景。
提示:選模板引擎不要只盯着性能。代码生成器跑一次最多几十毫秒,性能再差都无所谓。真正重要的是语法是否顺手、容错是否友好、团队有没有人熟悉。我们团队之前没人用过FreeMarker,但我会,所以选了它,后期带新人也有现成的参照。
2.2 自定义标签的设计思路
FreeMarker虽然强大,但直接用原生指令写模板,里面还是会有大量<#if>、<#list>嵌套,维护起来很痛苦。我在模板层之上封装了一套自定义指令,让模板看起来像是正常代码:
ftl复制// 原来的写法
<#list table.columns as col>
${col.javaField} ${col.javaType}
</#list>
// 封装后
<@genFieldList table.columns />
自定义指令的好处是,模板编写人员不需要懂FreeMarker语法,只需要认识几个简单的自定义标签就行。比如<@genImport />自动生成import语句,<@genEntityField />生成entity字段及注解,<@genQueryCondition />生成查询条件代码。这样后来维护模板的同事,只需要看我写好的标签说明就能上手。
2.3 代码合并策略的取舍
代码生成器最招人烦的一点是覆盖风险。比如生成Service后,又在里面手写了一段业务代码,结果因为改了个表结构重新生成,手写代码直接被覆盖了。针对这个问题我做了三个层级的取舍:
- 层级一:文件级保护。生成的时候先检查文件是否存在,默认情况下不覆盖,而是输出到
_generated备份目录,人工核对后再手动替换。 - 层级二:方法级追加。通过自定义标签,在模板里定位
// gen:custom:start和// gen:custom:end标记之间的代码,合并时保留该区域内的手写内容。 - 层级三:代码对比工具。生成完成后自动调起Beyond Compare或IDE的diff视图,让开发者直观看到差异,而不是默默覆盖。
实际使用中,第二级“方法级追加”用得最多,也最实用。比如一个分页查询方法,生成的默认逻辑是从表里无条件查询所有数据,但我往往要加一层数据权限过滤。这时候我只需要在模板里预留自定义区域,重新生成后那部分手写逻辑会原样保留,极其省心。
2.4 字段命名与类型映射的踩坑记录
这是我在项目里踩坑最多的地方。数据库字段明明叫user_name,转成Java后自然应该是userName,但如果表里有个字段叫uId,有的工具会生成uId,有的会生成uid,然后在Linux环境下的Git敏感大小写设置里莫名奇妙就把代码搞乱了,不得不统一规范。
我在元数据层专门做了一套命名转换规范,依赖开源的hutool工具的StrUtil.toCamelCase方法做底层转换,再包裹一层自己的规则处理:
java复制public class ColumnNameConverter {
public static String toJavaFieldName(String columnName) {
// 先走驼峰转换
String camelName = StrUtil.toCamelCase(columnName.toLowerCase());
// 处理特殊前缀,比如is_前缀去掉避免生成isIsDelete
if (c
