templatecenter:Spring Boot 与 FreeMarker 代码生成器
将单表元数据渲染为后端、前端和数据库脚本,记录模板管理与打包输出设计。
我用 Spring Boot + FreeMarker 写了一个代码生成器,给大学生课设用
为什么会有这回事
大二下学期课设季,朋友圈里全是求救消息。
“有没有人写过学生管理系统?求源码!” “哥们,我选题订的酒店管理系统,Spring Boot 到底怎么配啊?” “Vue 的路由到底怎么跳转?救救我。”
每年课设季都这样。同一批学生管理系统、酒店管理系统、图书馆管理系统,前人的代码在各种 QQ 群里传来传去,质量参差不齐,能跑就谢天谢地了。
我当时刚学完 Spring Boot,FreeMarker 也看了一点,就想着:能不能写个东西,把这些重复的 CRUD 代码自动生成出来?不用再让学生们复制粘贴别人的项目了。
于是 templatecenter 就这么来了。
技术选型
后端用 Spring Boot 3.1,Java 17,MyBatis-Plus 做 ORM。前端生成 Vue 3 + Vite + Element Plus 的代码。数据库脚本直接生成 MySQL 的 DDL。
核心思路很简单:用 Apache FreeMarker 的模板引擎,把一套写好的 .ftl 模板填上数据,输出用户能直接跑的完整项目。
为什么选 FreeMarker 而不用 Velocity 或者 Thymeleaf?说实话,FreeMarker 的模板语法最接近 Java 本身,写起来直觉,不需要额外学一套标签。而且它不依赖 Servlet 环境,纯 Java 就能跑,在代码生成这种场景下很方便。
整体架构
项目本身是个 Spring Boot Web 应用,分层组织方式(仅目录结构不能证明采用 DDD),大概长这样:
templatecenter/
├── src/main/java/com/templatecenter/
│ ├── controller/ # GeneratorController 接收前端请求
│ ├── service/ # GeneratorService 核心生成逻辑
│ ├── domain/ # DDD 领域模型
│ ├── infrastructure/ # 基础设施层,ZipUtils 在这一层
│ └── dto/ # 前后端交互的数据对象
├── src/main/resources/
│ ├── templates/ # FreeMarker 模板,.ftl 文件
│ └── application.yml
└── pom.xml
用户在前端页面上选好要生成什么表、什么字段,点一下"生成"按钮。后端拿到这些元数据,喂给 FreeMarker 模板引擎,模板引擎输出一堆文件,最后用 ZipUtils 打包成 zip 下载。
流程就这么直接。
代码生成逻辑
GeneratorService 是核心。用户传入的元数据(表名、字段列表、注释)会先被处理成 FreeMarker 的 DataModel,然后模板引擎挨个渲染。
关键代码大概是这个结构:
@Service
public class GeneratorService {
private final Configuration freemarkerCfg;
public GeneratorService() {
freemarkerCfg = new Configuration(Configuration.VERSION_2_3_32);
freemarkerCfg.setClassForTemplateLoading(this.getClass(), "/templates");
freemarkerCfg.setDefaultEncoding("UTF-8");
}
public void generateProject(GeneratorRequest request, OutputStream outputStream) {
// 1. 把用户输入的元数据转成 FreeMarker 能用的 Map
Map<String, Object> dataModel = buildDataModel(request);
// 2. 遍历模板列表,逐个渲染
List<String> templateFiles = listTemplates();
try (ZipOutputStream zos = new ZipOutputStream(outputStream)) {
for (String templateFile : templateFiles) {
Template template = freemarkerCfg.getTemplate(templateFile);
StringWriter sw = new StringWriter();
template.process(dataModel, sw);
// 3. 把渲染出来的内容塞进 zip
String outputPath = resolveOutputPath(templateFile, request);
zos.putNextEntry(new ZipEntry(outputPath));
zos.write(sw.toString().getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}
}
}
private Map<String, Object> buildDataModel(GeneratorRequest request) {
Map<String, Object> model = new HashMap<>();
model.put("tableName", request.getTableName());
model.put("className", toCamelCase(request.getTableName()));
model.put("fields", request.getFields());
model.put("projectName", request.getProjectName());
model.put("packageName", request.getPackageName());
model.put("date", LocalDate.now().toString());
return model;
}
}
这段代码做三件事:准备数据、渲染模板、打包输出。逻辑不复杂,但里面有不少细节值得说。
toCamelCase 这个方法会把 t_user_info 转成 UserInfo,把 order_detail 转成 OrderDetail。字段名、表名、类名之间的转换是代码生成器最烦人的部分之一。命名风格一旦确定下来,后面所有模板都靠这个统一的命名规则。
Zip 打包用的是 Java 原生的 ZipOutputStream,不需要引入额外依赖。每个模板渲染完就往 zip 里塞一个 entry,最终用户拿到的就是一个完整的、解压即用项目。
FreeMarker 模板
模板是这个项目的灵魂。每个 .ftl 文件对应一个输出文件。比如生成后端的 Controller:
package ${packageName}.controller;
import ${packageName}.domain.${className};
import ${packageName}.service.${className}Service;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/${tableName}")
public class ${className}Controller {
private final ${className}Service service;
public ${className}Controller(${className}Service service) {
this.service = service;
}
@GetMapping
public List<${className}> list() {
return service.list();
}
@GetMapping("/{id}")
public ${className} getById(@PathVariable Long id) {
return service.getById(id);
}
@PostMapping
public void save(@RequestBody ${className} entity) {
service.save(entity);
}
@PutMapping
public void update(@RequestBody ${className} entity) {
service.updateById(entity);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
service.removeById(id);
}
}
这个模板渲染出来就是一个标准的 Spring Boot REST Controller。五个接口,增删改查都有了,命名规范,该有的注解都在。
前端模板就更长一些了,.vue 文件里面要处理 Element Plus 的表格、表单、分页。不过核心思路一样:在模板里写死页面结构和交互逻辑,用 ${变量名} 把动态部分留出来。
比如前端列表页模板里,表头部分可能是这样:
<template>
<div class="app-container">
<el-table :data="tableData" v-loading="loading" border>
<#list fields as field>
<el-table-column prop="${field.fieldName}" label="${field.comment}" />
</#list>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<#list fields as field> 这个语法会遍历用户定义的字段列表,每个字段生成一列。用户选了五个字段,表格就有五列。选了八个,就是八列。
数据库脚本生成
除了前后端代码,数据库脚本也是生成的一部分。用户定义的字段会被拼成 MySQL DDL:
CREATE TABLE IF NOT EXISTS `${tableName}` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
<#list fields as field>
`${field.fieldName}` ${field.dbType} <#if field.nullable>NULL<#else>NOT NULL</#if> COMMENT '${field.comment}',
</#list>
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='${tableName}表';
这里每个动态字段之后保留逗号,因为后面还有 PRIMARY KEY;字段名、类型和注释还必须分别校验或转义。这种细节如果手写 SQL 脚本很容易出错,用模板就省心了。
踩过的坑
第一个大坑是 FreeMarker 的空值处理。用户可能不填某个字段,模板里直接 ${field.comment} 会报错。FreeMarker 对 null 值处理非常严格,必须显式处理。
解决办法是用 FreeMarker 的默认值语法:
${field.comment!''}
后面的 !'' 意思是如果 comment 为 null,就用空字符串。缺失值通常会触发模板异常,不能假定自动生成字符串 null;默认值语法也不替代注释转义。
第二个坑是编码。Windows 上默认编码是 GBK,FreeMarker 默认用 UTF-8,中文注释到模板里全是乱码。在 Configuration 里显式设置 setDefaultEncoding("UTF-8") 解决了。但如果用户的操作系统环境不同,偶尔还是会有问题。
第三个坑是模板路径。setClassForTemplateLoading 加载的是 classpath 下的模板,开发时 IDEA 里跑没问题,打成 jar 包部署的时候路径就不对了。最后改成在 jar 外面放一个 templates/ 目录,用文件路径加载,部署就稳定了。
第四个坑和 FreeMarker 无关,是 Zip 打包。ZipOutputStream 的 closeEntry 一定要调,不然 zip 文件会损坏。这个 bug 很隐蔽,解压的时候不一定报错,但解压出来的文件可能是空的。调试了好半天才发现。
一些想法
这个项目不算复杂,Spring Boot 的 Web 层 + FreeMarker 的模板引擎 + Java 的 Zip 输出,三样东西拼在一起就能跑。但做下来对"代码生成"这个概念有了不一样的理解。
代码生成的核心不是 AI,不是大模型,不是多高端的技术。核心是抽象。你得从一堆具体的项目里抽出共性:每个管理系统都有增删改查,每个表都有主键和创建时间,每个前端页面都有表格和表单。把这些共性抽象成模板,把变化的部分留成变量,代码生成器就成立了。
FreeMarker 在这里的角色就是"填空"。它不关心业务逻辑,不关心数据库连接,不关心用户认证。它只做一件事:把数据填进模板,输出文本。简单,但在这个场景下够用。
相比现在的 AI 代码生成,templatecenter 这种方式输出结果完全可控,输出由模板与输入共同决定,仍可能生成有缺陷的代码,不会突然给你换个框架。缺点也很明显:模板要自己维护,新功能要手写模板,没有 AI 那种"你说一句话我就给你生成"的灵活性。
但对于大学生课设来说,可控性比灵活性重要。学生要的是能跑、能交作业、能过答辩的东西,不是炫技。
下一步
现在 templatecenter 只支持单表 CRUD,不支持表关联。用户想生成一个"订单-订单明细"的一对多系统,现在还做不到。后面想加上关联关系定义,让模板能处理外键和嵌套查询。
另外模板现在都是硬编码在项目里的。想做成让用户可以自定义模板上传,这样有经验的开发者可以加自己的模板进来。
权限控制还没做,Sa-Token 的依赖加进去了但没接入。现在谁都能访问生成接口,生产环境肯定要加认证。
这些都是后面的事了。先把单表 CRUD 跑稳,再说其他的。
写在最后
templatecenter 是个小项目,没什么复杂的架构,没什么高深的技术。但它解决了一个真实的问题:每年课设季,总有一批学生在网上乱找源码,找到的要么跑不起来,要么缺文件,要么数据库连不上。
如果这个东西能帮他们省点时间,就够了。
代码在 GitHub 上,templatecenter,感兴趣可以看看。有问题提 issue,我一般隔天回。
课设季又快到了。希望今年的求救消息少一点。
生成器的校验边界
模板必须来自可信来源。包名、类名、SQL 标识符、字段类型和输出路径需要白名单或结构化校验;ZIP entry 不允许绝对路径或 … 路径穿越。classpath 加载可以用于打包 JAR,应检查资源是否正确打包;外置模板并非必需。示例方法省略异常处理与输入校验,生成项目还需编译和测试。