代码生成器这个东西,做后端开发的基本都躲不开。尤其是做企业管理类系统的朋友,一天到晚就是菜单、权限、增删改查,一两个模块手写还行,业务一多,几十张表铺过来,纯手敲真的能敲到怀疑人生。我自己早期也被这种重复劳动折磨过,后来花时间把代码生成器彻底吃透了,才发现原来一周的活可以压缩到一两天。这篇就把我在实际项目里折腾若依代码生成器前后端完整流程、源码逻辑、常见坑和一些二次开发思路都整理出来,给准备入坑或者正在用但用得不太顺的朋友一份参考。
1. 代码生成器到底解决了什么痛点
先说个实在的,代码生成器不是银弹,它解决的是"标准化重复劳动"的问题,不是"业务复杂度"的问题。很多人一听到代码生成器,要么觉得高大上,要么觉得没用,其实两种极端都不对。
1.1 从传统手写模式到自动化生成的转变
传统的手写模式大概是这样的:拿到需求,设计数据库,然后开始写 Entity、写 Mapper、写 Service、写 Controller、再写前端的 Vue 页面、API 接口调用。一个模块下来,少说十来个文件。如果是单表简单业务,这些代码八九成都是模板化的——就是改改类名、改改字段名、改改注释的事。
我见过不少团队,一开始都很自信,觉得代码量不大不需要生成器。结果业务迭代到中后期,几十张表、几百个接口,光维护这些重复代码的时间成本就非常吓人。而且人写多了容易疲劳,一疲劳就容易出低级错误,比如 WHERE 条件写错字段、DTO 里漏了字段、前端列表漏绑数据这类问题。
代码生成器的核心逻辑很简单:把数据库表结构作为输入,通过模板引擎渲染出标准化的代码文件。这个过程相当于把"人肉复制改参数"变成了"脚本自动生成",一致性更好,速度也快得多。
1.2 为什么我推荐从若依代码生成器入手
市面上代码生成工具有很多,有在线的,有 IDE 插件的,有公司自研的。但我个人建议先从若依的代码生成器入手,原因有三个。
第一个原因是它完整。若依的代码生成器不单单生成后端 /mapper、/service、/controller 那套,它连 Vue 的列表页、表单页、Api 封装文件,甚至数据库菜单的 SQL 脚本都一起生成。整个链条是闭的,生成完导入就能跑。
第二个原因是它跟若依的权限体系深度绑定。如果你用的是若依框架,用它的生成器生成出来的代码天然适配 @PreAuthorize 权限注解、菜单表 sys_menu 的按钮权限标识,不用自己再去手动关联。
第三个原因是它开源且模板可改。若依的生成逻辑不是黑盒,源码就在那里,模板文件用 Velocity 写的,想调整生成结果直接改模板就行,灵活度很高。
注意:如果你用的是非若依框架,也想借鉴它的思路,那你可以把它的
GenUtils、VelocityUtils这些核心逻辑单独拆出来,套到你自己的框架里去,这部分后面我会展开聊。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 若依代码生成器的整体设计与工作流程
用之前先搞明白它是怎么跑的。我之前见过不少同事,上来就点"生成代码",生成完发现不对,又不知道怎么改,本质就是没理解它的运行流程。
2.1 核心模块与数据表设计
若依的代码生成器主要由这几块组成:
| 模块 | 作用 | 关键接口/类 |
|---|---|---|
| 表信息管理 | 读取数据库表结构,保存在 gen_table 表中 |
IGenTableService |
| 列信息管理 | 读取表的列结构,保存在 gen_table_column 表中 |
IGenTableColumnService |
| 代码生成核心 | 组合表信息、列信息和模板,渲染出代码文件 | GenUtils、VelocityUtils |
| 模板管理 | 存放 vm 模板文件,定义各类代码文件的输出格式 |
vm/java/*.vm、vm/vue/*.vm |
这里面的核心数据表是 gen_table 和 gen_table_column,前者存表名、表注释、包名、模块名、业务名、功能名等配置,后者存每一列的属性名、字段类型、 Java 类型、是否列表显示、是否表单显示、查询方式等信息。
我第一次看的时候以为这些表的数据是手动维护的,后来才发现它有一个"导入表"的功能,你选中数据库里的一张物理表,它会自动读取该表的元数据(列名、数据类型、注释、主键等),自动填充到 gen_table_column 里,你可以在此基础上再微调。
2.2 从数据库表到代码文件的完整链路
整个流程我拆解一下,大家跟着思路走一遍就通了:
- 在数据库中建好业务表,比如要做一个客户管理,你得先有
biz_customer表。 - 登录若依后台,进入"系统工具 -> 代码生成",点击"导入",选择要生成的表。
- 系统调用
selectDbTableColumnsByTableName(不同数据库方言略有差异)读取物理表的列信息,写入gen_table_column表。 - 在代码生成列表中点击"编辑",配置基本信息:包名、模块名、业务名、功能名、生成类型(哪张
Controller返回类型)、父子表,以及每列的属性(是否必填、是否列表、查询类型等)。 - 点击"生成代码",后端从
gen_table和gen_table_column读取配置,结合包名和模块名拼接出文件路径和类名。 VelocityUtils根据模板类别获取到模板列表,对每个模板进行Velocity渲染。- 渲染完成后,把所有模板文件打成
zip包返回给前端下载。
这个过程最核心的点在于模板渲染。模板里通过 $table.xxx、$column.xxx 这类占位符引用配置对象里的属性,渲染引擎把占位符替换成实际值,最终形成可运行的代码文件。
2.3 为什么选择 Velocity 作为模板引擎
刚接触若依代码生成器源码的朋友可能会问:为什么它用 Velocity 而不是 FreeMarker 或者 Thymeleaf?
这个我觉得主要还是历史原因和技术选型的权衡。若依这个框架对技术栈的要求是轻量、简单、够用,Velocity 的语法非常简单,对于模板里大多是 if/else、foreach 这种控制语句的场景来说已经足够了。而且 Velocity 的模板文件不需要经过像 JSP 那样的容器编译,直接基于文本模板渲染,启动和调试成本低。
在做二次开发时,如果你想换模板引擎,其实只需要改掉 VelocityUtils 中创建 VelocityContext 和 Velocity 引擎初始化的部分,换成 FreeMarker 的 Template 加载逻辑即可,其他业务逻辑不用动。不过说实话,我用了这么久,Velocity 够用了,没有必要为了换而换。
3. 实操:一次完整的代码生成全流程
光讲原理有点虚,这里我把实际操作的完整步骤捋一遍,截图级别的详细度,照着做基本上都能跑通。
3.1 准备阶段:数据库建表与基础配置
第一步肯定是建表。举个实际的例子,我要做一个简单的"产品分类"管理,建表 SQL 大概长这样:
sql复制CREATE TABLE `biz_product_category` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
`parent_id` bigint(20) DEFAULT NULL COMMENT '父分类ID',
`category_name` varchar(100) NOT NULL COMMENT '分类名称',
`sort_no` int(11) DEFAULT 0 COMMENT '排序号',
`status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='产品分类表';
有几个小细节要提醒一下。第一,表注释一定要写,它会被当成本模块的"功能描述"用。第二,字段注释也尽量写全,因为生成出来的代码里 @Excel 注解的 name、表单的 label、列表的表头,都直接取自字段注释。第三,字段命名尽量规范,用下划线风格,若依会自动帮你转换成驼峰。如果你用了一些奇怪的缩写,生成出来的类名和属性名会很难看,后期改起来还麻烦。
3.2 导入表并调整字段配置
进入 系统工具 -> 代码生成,点击"导入"按钮,会弹出数据库里没有被导入过的表列表,选中 biz_product_category,保存。此时该表会出现在代码生成列表里,但你还要编辑配置,不然生成结果往往是默认的、没法直接用的。
点击"编辑",这里有几个关键配置项:
- 基本信息:包名(如
com.ruoyi.biz)、模块名(如biz)、业务名(如category)、功能名(如产品分类)。这些决定了最终代码的包结构和类名。注意业务名尽量用单数,别用复数。 - 生成方式:默认是"zip压缩包下载",也可以选"自定义路径",直接把文件输出到本地磁盘的指定项目目录下。多人协作时我建议用
zip下载,避免直接写进别人的代码目录产生冲突。 - 上级菜单:选择生成代码里的菜单挂在哪个菜单下。它会生成一段
menu.sql,里面包含菜单、按钮的基础SQL。 - 字段配置:这里很重要。点开字段列表,可以看到从
biz_product_category表读取到的列信息。你需要逐列检查:category_name:必填勾上,列表显示勾上,查询类型选"等于",HTML类型选input。status:列表显示、表单显示都勾上,HTML类型选radio,字典类型选sys_normal_disable,这样生成出来就是单选框而不是文本框。create_by、create_time这些审计字段:列表显示可以勾上,但表单显示不要勾,因为它们是后端自动填充的。parent_id这种树表结构字段:如果你要做树表,在基本信息里选择"树编码字段"为parent_id,"树名称字段"为category_name,不要勾选parent_id的列表和表单显示,否则列表展示会多出来一个没太大意义的列。
这些配置直接决定生成代码的体验,我第一次用的时候没仔细配置,结果字段全默认,生成的 Vue 页面表单里连 create_time 都要手动填,直接被同事吐槽。
3.3 下载生成代码并集成到项目中
配置完成后,回到代码生成列表,点击"生成代码",浏览器会下载一个 ruoyi.zip(名字取决于当前生成的表)。解压出来的结构非常清楚:
code复制main/
├── java/com/ruoyi/biz/
│ ├── controller/BizProductCategoryController.java
│ ├── domain/BizProductCategory.java
│ ├── mapper/BizProductCategoryMapper.java
│ ├── service/IBizProductCategoryService.java
│ └── service/impl/BizProductCategoryServiceImpl.java
├── resources/mapper/biz/BizProductCategoryMapper.xml
├── sql/menu.sql
└── vue/
├── api/biz/productCategory.js
└── views/biz/category/index.vue
把这个包里的文件复制到你实际项目的对应目录下,然后在数据库执行 sql/menu.sql,刷新菜单权限就能看到新模块了。
有一点务必注意:menu.sql 里的 menu_id 是固定写死的,如果你数据库里已经有这些 ID,导入时主键会冲突。建议导入前先检查一下,或者直接改掉 menu_id 再执行,否则会报错或者覆盖原有菜单。
3.4 集成后的前后端功能验证
集成完成后的验证步骤也很重要,别急着完事。你先启动后端服务,用 Postman 或者直接登录系统调一下接口。验证点有这么几个:
- 后端接口是否正常:比如
GET /biz/category/list能否正确返回分页数据,POST /biz/category能否新增数据。 - 权限注解是否生效:生成的
Controller每个方法上都有@PreAuthorize("@ss.hasPermi('biz:category:list')")这类注解,你要确保当前登录用户分配了对应权限,否则会返回 403。 - 前端页面是否能跑通:访问菜单,看列表加载、新增弹窗、编辑回显、删除确认这些基本功能是否正常。
这个过程其实也是检验你前面字段配置是否合理的过程。比如你发现列表页某些列根本不需要展示,那就要回去改 gen_table_column 里的"列表显示"标识,重新生成,而不是在前端页面里手工删掉——你手工改了,下次重新生成就没了,而配置改了是持久化的。
4. 核心逻辑的源码级拆解与二次开发思路
说句实话,会用生成器不算厉害,能根据自己的需求去改生成器,才是把这工具吃透了。这一部分我会挑几个关键的源码点来拆解,顺带讲怎么二次开发。
4.1 表数据读取:数据库元信息是怎么变成业务配置的
所有生成的起点是读取数据库表结构,这个逻辑在若依里对应 GenTableServiceImpl.selectDbTableColumnsByTableName 这个方法。不同类型的数据库实现不一样,但核心都是查询系统表或信息模式表,把表名、列名、数据类型、键信息取出来。
以 MySQL 为例,核心的查询逻辑是通过 information_schema 库来获取:
sql复制select
column_name, data_type, column_comment, column_key
from information_schema.columns
where table_schema = database() and table_name = #{tableName}
order by ordinal_position
拿到这些原始信息之后,代码里会做一层映射转换。这一步在 GenUtils.initColumnField 方法里,核心思路是:
- 把数据库字段名转成驼峰式
Java属性名(如category_name->categoryName)。 - 根据数据库类型映射
Java类型,比如varchar->String,bigint->Long,datetime->Date。 - 根据列名判断是否是插入、编辑、列表必填字段。比如
create_time、update_by这类系统字段默认不作为表单字段。 - 根据注释推测字段的
HTML类型,比如注释中包含"时间"就尝试映射成datetime控件。
这里有个特别值得关注的点,就是数据库注释的重要性。很多人建表的时候偷懒不写注释,导入之后 columnComment 为空,生成的代码注释、表头、表单标签全是空的,整个代码质量立刻掉档。所以用好代码生成器的前提是有一个良好的建表习惯。
4.2 代码渲染:Velocity 模板引擎的工作机制
VelocityUtils 是代码生成最重要的工具类。它做了两件核心的事:
第一,根据配置组装模板列表。不同类型的生成任务(比如单表、树表、主子表)对应不同的模板组合。单表的话,模板包括所有基础文件;树表的话还要额外生成树结构相关的处理代码。
第二,创建 VelocityContext 并放入模板渲染所需的数据。这里不只是把 genTable、columns 丢进去,而是做了一层数据加工:
java复制VelocityContext context = new VelocityContext();
context.put("tableName", genTable.getTableName());
context.put("ClassName", genTable.getClassName());
context.put("className", genTable.getClassNameLowerCase());
context.put("moduleName", genTable.getModuleName());
context.put("businessName", genTable.getBusinessName());
context.put("functionName", genTable.getFunctionName());
context.put("columns", genTable.getColumns());
context.put("packageName", genTable.getPackageName());
然后逐一渲染模板:
java复制for (String template : templates) {
// 渲染文件路径
String filePath = getFileName(template, genTable);
// 渲染文件内容
String content = VelocityEngineUtils.mergeTemplate(velocityEngine, template, "UTF-8", context);
// 将内容写入 zip 输出流
...
}
这里我觉得最值得学习的是 getFileName 这个方法,它通过模板路径和表配置动态拼接出目标文件路径。比如模板路径是 vm/java/domain.java.vm,传入的表配置是包名 com.ruoyi.biz、模块名 biz、业务名 category,它就能拼出:
code复制main/java/com/ruoyi/biz/domain/BizProductCategory.java
这种设计思路很干净,模板只关心文件内容长什么样,文件名和路径完全由配置逻辑生成,两者解耦,非常好扩展。
4.3 自定义模板:生成符合团队规范的代码
实际项目中,直接用默认模板生成的代码,往往跟团队规范有些出入。举个例子,有些团队统一用 Lombok,不写 getter/setter;有些团队要求在 Controller 层统一加 @ApiOperation;有些团队要求 Service 接口里必须有分页注释。这些都能通过改模板来解决。
若依的模板文件在 resources/vm 目录下,主要有这些:
| 模板文件 | 生成目标 |
|---|---|
java/domain.java.vm |
实体类 |
java/mapper.java.vm |
MyBatis Mapper 接口 |
java/service.java.vm |
Service 接口 |
java/serviceImpl.java.vm |
Service 实现类 |
java/controller.java.vm |
Controller 控制层 |
java/mapper.xml.vm |
Mapper XML 文件 |
vue/index.vue.vm |
Vue 列表页面 |
vue/api.ts.vm |
前端 API 封装 |
比如你希望实体类用 Lombok,只需要把 domain.java.vm 里的 getter/setter 循环删掉,加上 @Data 注解:
vm复制@Data
public class ${ClassName} extends BaseEntity {
private static final long serialVersionUID = 1L;
#foreach($column in $columns)
/** $column.columnComment */
private $column.javaType $column.javaField;
#end
}
这里要注意一个问题:模板是全局生效的,改了之后影响后续所有代码生成。所以团队里有多个项目,模板规范不统一时,建议在代码生成器配置里增加一个"应用类型"字段,根据不同的类型加载不同的模板目录,这个可以通过扩展 VelocityUtils 来实现。比如加一个 templateType 开关,根据开关值选择从 vm/standard 还是 vm/lombok 目录加载模板。
4.4 扩展一个全新的模板类型
若依默认支持的生成模板类型包括 crud(单表)、tree(树表)、sub(主子表)。如果你业务中经常出现其他固定模式,比如"导入导出型单表",你可以自己加类型。
实现起来大概需要这几步:
- 在
gen_table表的tpl_category字段里增加枚举值映射(有的是String,有的是Integer,看版本)。 - 在
VelocityUtils.getTemplateList方法里,按新的tplCategory增加一个case分支,返回你自定义的模板列表。 - 编写对应的
vm模板文件,放到vm/目录下。 - 如果是 Java 代码里写死了判断,需要把前端
index.html或者模板里对应的下拉框选项补上,让用户能选择。
这里有个经验,加模板类型前先想清楚:这个模板类型和默认 crud 生成的代码差异大不大?如果只是个别文件不同,比如只是 Controller 多一个 @Log 注解,那就直接在原有模板上加 if 判断控制即可,不需要单独搞一套。避免类型过多、维护成本爆炸。
5. 常见问题与排查技巧实录
用代码生成器过程中,有几个经典问题,几乎所有人都踩过。这里把排查思路也写出来,方便大家参考。
5.1 生成的菜单 SQL 导入报错或菜单重复
这个问题出现频率很高。menu.sql 里的菜单 ID 是固定的,每次生成都生成相同的 ID,如果你多次生成同一个表的菜单,数据库里就会有重复的 menu_id,导入时要么主键冲突,要么菜单权限混乱。
我的建议是这样:生成代码之前,确认一下这个模块的菜单是不是已经导入过了。如果导入过,第二次生成就不要执行 menu.sql,而是手动在菜单管理里调整。如果你希望每次生成动态分配 menu_id,可以改动生成 menu.sql 的逻辑,用 select max(menu_id)+1 这种方式来生成,但这需要改源码,不推荐新手一上来就搞。
提示:在生成列表里"编辑"功能的
菜单下拉框默认会选择一个父菜单ID,这时候生成的 SQL 里 parent_id 就对应这个值。如果你不改,默认是根菜单。所以编辑的时候,把上级菜单选对了,生成的 SQL 才是你真正想要的。
5.2 代码生成后页面无法访问,接口 404
这种问题先排查后端:启动时是否有报错,Mapper.xml 路径是否正确,@MapperScan 是否能扫到新生成的 Mapper 接口。然后看前端:API 文件里的请求路径是否跟后端 Controller 的 @RequestMapping 值一致。
我遇到过好几次,问题出在包名不一致上。比如生成的时候包名填了 com.ruoyi.biz,但你的项目实际模块路径是 com.company.system,那就可能导致组件扫描不到,或者路径不匹配。所以生成前一定要确认好包名。
5.3 生成代码中的字段类型对不上
比如数据库里有个 json 类型的字段,若依默认的代码生成器可能不支持,把它映射成 String 或者 Object,这就可能导致后续处理 json 结构时很别扭。
这种情况下,要么你自己在字段配置里手动指定 Java 类型,要么修改 GenUtils 里的类型映射方法,把 json 映射为 String 并且在 Mapper.xml 里配置成 jdbcType=VARCHAR(或者你用 MyBatis 的 TypeHandler 做自动转换)。看你的具体业务复杂度来定。
5.4 生成出来的 Excel 导出功能某些字段格式不对
若依的 Excel 注解是基于字段反射来识别列的,如果你字段类型是 BigDecimal,默认可能导出的是普通数值,没有保留两位小数很好控制。你可以把 @Excel 注解改成:
java复制@Excel(name = "金额", scale = 2)
private BigDecimal amount;
这就是改模板里 domain.java.vm 里 @Excel 生成的策略。在你自己的代码生成器里,给 gen_table_column 增加一个"保留小数位数"配置,生成时自动加上 scale 属性,会比自己每次手动去改注解省事很多。
5.5 代码生成器对 Oracle 或者其他数据库的支持问题
如果你用的是 Oracle,这份代码生成器也是支持的,若依的 sql 脚本和 GenUtils 里都有对应的适配逻辑。不过要注意,Oracle 把表名、列名默认转成大写,导致表注释、列注释可能在信息读取时大小写不一致。
这个时候,你在 gen_table_column 里看到的字段名可能会全部变成大写。解决办法有两个:一个是在导入后手动把列名转成驼峰大小写风格,另一个是在 GenUtils 里转成统一的处理规则。我的建议是,如果不是非用 Oracle 不可,MySQL 的体验会顺畅很多,毕竟这个框架的默认推荐数据库也是 MySQL,踩坑文档也最多。
6. 代码生成器的进一步扩展玩法
最后简单聊几个我觉得比较实用的进阶方向。
6.1 让生成结果带上公司的统一返回结构
很多公司的接口返回值不是若依默认的 AjaxResult,而是自己的 ResponseResult<T>,比如带 code/message/data/traceId 这种结构。这种情况直接把生成的 Controller 模板改一下,把返回值类型从 AjaxResult 换成自己的包装类,同时把 Service 层的方法返回类型也调整一致。模板改好后,只需要在 gen_table 里增加一个 CRUD 风格字段,比如 responseType,就能做到一套代码兼容多种返回结构。
6.2 结合 Swagger/Knife4j 自动生成接口文档
若依的 Controller 里默认没有 @Api、@ApiOperation 注解(具体看版本,如果是前后端分离版本,有些模板里有)。如果你们的接口文档工具是 Knife4j,那你自然希望在生成的代码里自动带上这些注解。
这个改动其实也很简单,直接在 controller.java.vm 里加注解导入,然后在类上和方法上加上 @Api、@ApiOperation,注释内容取表注释和字段注释。这样文档直接打开就是中文描述,后端同学就不用再手动补一遍了。
6.3 把生成器接入到自动化交付流水线
如果你们团队的代码提交是走 GitLab CI 的,可以在流水线里做一个"代码生成"任务。开发者在后台配置好表,提交生成请求,后端自动生成代码、自动打 zip、自动解压到特定分支、甚至自动提交 PR。这块实现起来并不复杂,核心就是把 GenUtils 集成到一个独立的 Spring Boot 服务里,通过接口对外提供生成能力,然后配一个 Shell 或者 Python 脚本来处理后置动作。
不过说实话,大多数团队用不上这么高级的玩法。先把后台代码生成器用顺了,把模板改成团队规范一致,效率已经能提升一大截了。如果后续项目规模化、模块众多,再考虑要不要做自动化流水线也不迟。
写在最后的个人体会
我前前后后负责过好几个项目的代码生成器落地,最大的体会是:工具好不好用,七分看配置,三分看模板。很多人觉得代码生成器生成的东西质量差,其实往往是字段配置不认真、建表随意、模板不符合实际项目规范导致的。
同样一张表,前期花十分钟把字段的必填、列表、查询类型、字典类型都配好,把模板改成符合团队的规范,生成出来的代码可以直接进 Code Review,几乎不用改。而如果为了赶进度,导入表之后一路点确定,生成的代码自然需要后期大量手改,那还不如一开始手写了。
如果你现在还没用过代码生成器,或者用过但体验不好,建议你找个简单的业务表,按照我这篇文章的步骤重新走一遍。把字段配置、模板定制、前后端联调整条链路跑通一次,你会发现这个工具能省下的时间远超你的预期。我后来在团队里推这套流程,不少一开始抵触的同事,用顺手之后都回来跟我说"真香",希望这篇内容也能让你少走一些弯路。
