代码生成模板自定义与扩展指南
代码生成模板自定义与扩展指南
引言
代码生成是低代码平台的核心能力,而模板引擎是代码生成的灵魂。通过自定义模板,开发者可以根据团队规范、项目架构和业务需求,快速生成标准化的代码骨架,大幅减少重复编码工作。
本文将对比 FreeMarker、Velocity、Thymeleaf 三种主流模板引擎,介绍自定义模板开发规范、模板集管理、参数配置和版本管理的最佳实践。
核心内容
一、三种模板引擎对比
graph TD
A[模板引擎选型] --> B[FreeMarker]
A --> C[Velocity]
A --> D[Thymeleaf]
B --> B1[功能最强大]
B --> B2[语法灵活]
B --> B3[社区活跃]
C --> C1[语法最简洁]
C --> C2[学习成本低]
C --> C3[功能较基础]
D --> D1[自然模板]
D --> D2[浏览器可预览]
D --> D3[适合HTML生成]
style B fill:#c8e6c9
| 特性 | FreeMarker | Velocity | Thymeleaf |
|---|---|---|---|
| 语法复杂度 | 中等 | 简单 | 中等 |
| 功能丰富度 | ★★★★★ | ★★★ | ★★★★ |
| 宏/函数支持 | 强大 | 基础 | 中等 |
| 条件/循环 | 完善 | 完善 | 完善 |
| 输出转义 | 自动 | 手动 | 自动 |
| 模板继承 | 支持 | 不支持 | 支持 |
| 浏览器预览 | 不支持 | 不支持 | 支持 |
| Spring集成 | 良好 | 一般 | 原生 |
| 代码生成适用 | ★★★★★ | ★★★★ | ★★★ |
推荐:代码生成场景首选 FreeMarker,功能强大且生态成熟。
二、FreeMarker 模板开发规范
1. 模板目录结构
templates/
├── java/
│ ├── controller.ftl # Controller模板
│ ├── service.ftl # Service接口模板
│ ├── serviceImpl.ftl # Service实现模板
│ ├── mapper.ftl # Mapper接口模板
│ ├── entity.ftl # 实体类模板
│ ├── dto/
│ │ ├── createDTO.ftl # 创建DTO模板
│ │ └── updateDTO.ftl # 更新DTO模板
│ └── vo.ftl # VO模板
├── vue/
│ ├── api.ftl # API请求模板
│ ├── index.ftl # 页面模板
│ └── components/
│ ├── form.ftl # 表单组件模板
│ └── table.ftl # 表格组件模板
├── xml/
│ └── mapper.ftl # MyBatis XML模板
└── config/
└── template-config.json # 模板配置文件
2. 模板配置文件
{
"templateSet": {
"code": "standard-crud",
"name": "标准CRUD模板集",
"description": "生成完整的CRUD代码,包含前后端",
"version": "1.0.0",
"templates": [
{
"id": "controller",
"name": "Controller",
"templatePath": "java/controller.ftl",
"outputPath": "${basePackagePath}/controller/${className}Controller.java",
"fileType": "java"
},
{
"id": "service",
"name": "Service接口",
"templatePath": "java/service.ftl",
"outputPath": "${basePackagePath}/service/${className}Service.java",
"fileType": "java"
},
{
"id": "serviceImpl",
"name": "Service实现",
"templatePath": "java/serviceImpl.ftl",
"outputPath": "${basePackagePath}/service/impl/${className}ServiceImpl.java",
"fileType": "java"
},
{
"id": "vue-page",
"name": "Vue页面",
"templatePath": "vue/index.ftl",
"outputPath": "views/${moduleName}/${businessName}/index.vue",
"fileType": "vue"
}
],
"params": [
{
"code": "basePackage",
"name": "基础包路径",
"type": "STRING",
"defaultValue": "com.example",
"required": true
},
{
"code": "moduleName",
"name": "模块名称",
"type": "STRING",
"required": true
},
{
"code": "useRecord",
"name": "使用Record类",
"type": "BOOLEAN",
"defaultValue": true
},
{
"code": "author",
"name": "作者",
"type": "STRING",
"defaultValue": "CodeGenerator"
}
]
}
}
3. Controller 模板示例
<#-- Controller模板 -->
package ${basePackage}.${moduleName}.controller;
import ${basePackage}.${moduleName}.service.${className}Service;
import ${basePackage}.${moduleName}.dto.${className}CreateDTO;
import ${basePackage}.${moduleName}.dto.${className}UpdateDTO;
import ${basePackage}.${moduleName}.vo.${className}VO;
import ${basePackage}.common.core.Result;
import ${basePackage}.common.core.PageResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* ${tableComment}管理
*
* @author ${author}
* @date ${.now?string('yyyy-MM-dd')}
*/
@RestController
@RequestMapping("/api/${moduleName}/${businessName}")
@Tag(name = "${tableComment}管理")
@RequiredArgsConstructor
public class ${className}Controller {
private final ${className}Service ${className?uncap_first}Service;
@Operation(summary = "分页查询")
@GetMapping("/page")
public Result<PageResult<${className}VO>> pageQuery(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
<#list queryFields as field>
@RequestParam(required = false) ${field.javaType} ${field.fieldName}<#if field?has_next>,</#if>
</#list>) {
return Result.success(${className?uncap_first}Service.pageQuery(page, size<#list queryFields as field>, ${field.fieldName}</#list>));
}
@Operation(summary = "根据ID查询")
@GetMapping("/{id}")
public Result<${className}VO> getById(@PathVariable Long id) {
return Result.success(${className?uncap_first}Service.getById(id));
}
@Operation(summary = "创建")
@PostMapping
public Result<Void> create(@RequestBody @Valid ${className}CreateDTO dto) {
${className?uncap_first}Service.create(dto);
return Result.success();
}
@Operation(summary = "更新")
@PutMapping
public Result<Void> update(@RequestBody @Valid ${className}UpdateDTO dto) {
${className?uncap_first}Service.update(dto);
return Result.success();
}
@Operation(summary = "删除")
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
${className?uncap_first}Service.delete(id);
return Result.success();
}
}
4. Entity 模板示例(支持Record)
<#if useRecord>
<#-- Record类模板 -->
package ${basePackage}.${moduleName}.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* ${tableComment}实体
*
* @author ${author}
* @date ${.now?string('yyyy-MM-dd')}
*/
@TableName("${tableName}")
public record ${className}(
<#list fields as field>
<#if field.primaryKey>
@TableId(type = IdType.ASSIGN_ID)
</#if>
<#if field.columnName != field.fieldName>
@TableField("${field.columnName}")
</#if>
${field.javaType} ${field.fieldName}<#if field?has_next>,</#if>
</#list>
) {}
<#else>
<#-- 传统类模板 -->
package ${basePackage}.${moduleName}.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* ${tableComment}实体
*
* @author ${author}
* @date ${.now?string('yyyy-MM-dd')}
*/
@Data
@Accessors(chain = true)
@TableName(value = "${tableName}"<#if hasJsonField>, autoResultMap = true</#if>)
public class ${className} {
<#list fields as field>
<#if field.primaryKey>
@TableId(type = IdType.ASSIGN_ID)
</#if>
<#if field.columnName != field.fieldName>
@TableField("${field.columnName}"<#if field.jsonField>, typeHandler = JacksonTypeHandler.class</#if>)
</#if>
/** ${field.comment} */
private ${field.javaType} ${field.fieldName};
</#list>
}
</#if>
三、模板引擎集成
/** 模板引擎服务 */
@Service
public class TemplateEngineService {
private final Configuration freemarkerConfig;
public TemplateEngineService() throws IOException {
this.freemarkerConfig = new Configuration(Configuration.VERSION_2_3_32);
this.freemarkerConfig.setDirectoryForTemplateLoading(
new File("templates/"));
this.freemarkerConfig.setDefaultEncoding("UTF-8");
this.freemarkerConfig.setTemplateExceptionHandler(
TemplateExceptionHandler.RETHROW_HANDLER);
}
/**
* 渲染模板
* @param templateName 模板名称
* @param dataModel 数据模型
* @return 渲染结果
*/
public String render(String templateName, Map<String, Object> dataModel) {
try {
Template template = freemarkerConfig.getTemplate(templateName);
StringWriter writer = new StringWriter();
template.process(dataModel, writer);
return writer.toString();
} catch (TemplateException | IOException e) {
throw new RuntimeException("模板渲染失败: " + templateName, e);
}
}
/**
* 批量渲染模板集
* @param templateSet 模板集配置
* @param dataModel 数据模型
* @return 渲染结果列表
*/
public List<GeneratedFile> renderTemplateSet(
TemplateSetConfig templateSet,
Map<String, Object> dataModel) {
List<GeneratedFile> results = new ArrayList<>();
for (TemplateConfig template : templateSet.getTemplates()) {
String content = render(template.getTemplatePath(), dataModel);
String outputPath = renderOutputPath(template.getOutputPath(), dataModel);
results.add(new GeneratedFile(
template.getName(),
outputPath,
content,
template.getFileType()
));
}
return results;
}
/** 渲染输出路径中的变量 */
private String renderOutputPath(String pathTemplate, Map<String, Object> dataModel) {
String result = pathTemplate;
for (Map.Entry<String, Object> entry : dataModel.entrySet()) {
result = result.replace("${" + entry.getKey() + "}",
String.valueOf(entry.getValue()));
}
return result;
}
}
四、数据模型构建
graph TD
A[数据库表元数据] --> B[数据模型构建器]
C[用户配置参数] --> B
D[模板集配置] --> B
B --> E[数据模型]
E --> F[className]
E --> G[tableName]
E --> H[fields列表]
E --> I[basePackage]
E --> J[moduleName]
E --> K[自定义参数]
F --> L[模板渲染]
G --> L
H --> L
I --> L
J --> L
K --> L
/** 代码生成数据模型构建器 */
@Service
public class CodegenDataModelBuilder {
/**
* 构建代码生成数据模型
* @param tableInfo 数据库表信息
* @param params 用户配置参数
* @return 数据模型
*/
public Map<String, Object> build(TableInfo tableInfo, CodegenParams params) {
Map<String, Object> model = new HashMap<>();
// 基础信息
model.put("className", toClassName(tableInfo.getTableName()));
model.put("tableName", tableInfo.getTableName());
model.put("tableComment", tableInfo.getTableComment());
model.put("businessName", toBusinessName(tableInfo.getTableName()));
model.put("moduleName", params.getModuleName());
model.put("basePackage", params.getBasePackage());
model.put("basePackagePath", params.getBasePackage().replace(".", "/"));
model.put("author", params.getAuthor());
model.put("useRecord", params.isUseRecord());
// 字段信息
List<FieldInfo> fields = tableInfo.getColumns().stream()
.map(this::convertField)
.toList();
model.put("fields", fields);
// 查询字段(用于分页查询参数)
List<FieldInfo> queryFields = fields.stream()
.filter(f -> f.isQueryable())
.toList();
model.put("queryFields", queryFields);
// 表单字段(用于创建/更新DTO)
List<FieldInfo> formFields = fields.stream()
.filter(f -> !f.isPrimaryKey() && !f.isAutoFill())
.toList();
model.put("formFields", formFields);
return model;
}
/** 表名转类名:sys_user -> SysUser */
private String toClassName(String tableName) {
String[] parts = tableName.split("_");
StringBuilder sb = new StringBuilder();
for (String part : parts) {
if (part.isEmpty()) continue;
sb.append(Character.toUpperCase(part.charAt(0)))
.append(part.substring(1));
}
return sb.toString();
}
}
五、模板集管理
graph TD
A[模板集管理] --> B[模板集列表]
A --> C[模板集详情]
A --> D[模板集版本]
B --> B1[标准CRUD模板集]
B --> B2[树形结构模板集]
B --> B3[主子表模板集]
C --> C1[包含模板列表]
C --> C2[参数配置]
C --> C3[输出路径规则]
D --> D1[v1.0 初始版本]
D --> D2[v1.1 修复问题]
D --> D3[v2.0 架构升级]
/** 模板集管理服务 */
@Service
public class TemplateSetService {
/**
* 获取模板集列表
*/
public List<TemplateSetVO> listTemplateSets() {
// 从数据库或文件系统加载模板集列表
return templateSetMapper.selectList(null).stream()
.map(this::toVO)
.toList();
}
/**
* 预览代码生成结果
* 不实际生成文件,仅返回预览内容
*/
public List<GeneratedFile> preview(Long templateSetId, CodegenParams params) {
TemplateSetConfig config = getTemplateSetConfig(templateSetId);
Map<String, Object> dataModel = dataModelBuilder.build(
getTableInfo(params.getTableId()), params);
return templateEngineService.renderTemplateSet(config, dataModel);
}
/**
* 执行代码生成
* 将生成的代码写入指定目录
*/
public void generate(Long templateSetId, CodegenParams params, String outputDir) {
List<GeneratedFile> files = preview(templateSetId, params);
for (GeneratedFile file : files) {
Path filePath = Paths.get(outputDir, file.getOutputPath());
// 确保目录存在
Files.createDirectories(filePath.getParent());
// 写入文件
Files.writeString(filePath, file.getContent());
}
}
}
六、模板版本管理
/** 模板版本实体 */
public record TemplateVersion(
Long id,
String templateCode,
String version,
String content,
String changeLog,
Boolean isActive,
LocalDateTime createTime
) {}
/** 模板版本管理服务 */
@Service
public class TemplateVersionService {
/**
* 发布新版本
*/
@Transactional
public void publishVersion(String templateCode, String content, String changeLog) {
// 将当前活跃版本置为非活跃
templateVersionMapper.deactivateByCode(templateCode);
// 创建新版本
TemplateVersion version = new TemplateVersion(
null,
templateCode,
generateNextVersion(templateCode),
content,
changeLog,
true,
LocalDateTime.now()
);
templateVersionMapper.insert(version);
}
/**
* 回滚到指定版本
*/
@Transactional
public void rollback(String templateCode, String targetVersion) {
templateVersionMapper.deactivateByCode(templateCode);
templateVersionMapper.activateByCodeAndVersion(templateCode, targetVersion);
}
}
结论与建议
模板开发规范
-
命名规范:模板文件名使用小写+连字符,如
service-impl.ftl;变量使用驼峰命名。 -
注释规范:模板文件头部添加说明注释,包含模板用途、参数说明、作者信息。
-
条件控制:使用
<#if>控制可选代码段,通过参数开关控制生成内容。 -
格式规范:模板中的缩进与目标语言规范一致,避免生成代码格式混乱。
-
安全输出:使用
${variable!}处理空值,使用?html或?string转义输出。
最佳实践建议
-
模板集而非单模板:按业务场景组织模板集,一个模板集包含前后端所有相关文件。
-
预览优先:生成代码前先预览,确认无误后再写入文件。
-
不覆盖已有文件:生成代码时检测文件是否已存在,已存在则跳过或提示用户确认。
-
版本回滚:保留模板历史版本,支持一键回滚。
-
参数化一切:所有可变内容都应通过参数控制,模板中不硬编码任何业务数据。