FreeMarker 模板引擎实现代码生成器:自动生成 Entity/DTO/Controller/Vue 全栈代码
FreeMarker模板引擎在代码生成器中的应用
引言
在低代码平台中,代码生成器是提升开发效率的核心引擎。它能够根据数据库表结构,自动生成从后端到前端的完整CRUD代码,将重复性的编码工作压缩到秒级完成。FreeMarker作为成熟的模板引擎,以其简洁的语法、强大的指令集和良好的扩展性,成为代码生成器的理想选择。
本文将深入讲解FreeMarker模板语法,分析代码生成器如何利用模板引擎生成Entity/DTO/Mapper/Service/Controller/Vue等各层代码,并给出实用的模板示例。
一、FreeMarker核心概念
1.1 模板引擎工作原理
flowchart TD
A[数据库表元数据] --> B[数据模型构建]
C[FreeMarker模板文件] --> D[模板引擎解析]
B --> D
D --> E[模板 + 数据模型合并]
E --> F[生成目标代码文件]
subgraph 数据模型
B1[表名/字段名]
B2[字段类型映射]
B3[包路径配置]
B4[作者/日期]
end
subgraph 模板文件
C1[Entity.ftl]
C2[DTO.ftl]
C3[Mapper.ftl]
C4[Service.ftl]
C5[Controller.ftl]
C6[Vue.ftl]
end
A -.-> B1
A -.-> B2
C1 -.-> C
C2 -.-> C
C3 -.-> C
C4 -.-> C
C5 -.-> C
C6 -.-> C
style D fill:#4CAF50,color:#fff
style E fill:#2196F3,color:#fff
1.2 FreeMarker基本语法速览
<#-- 注释:不会输出到结果中 -->
<#-- 1. 插值表达式 -->
类名:${className}
表名:${tableName}
<#-- 2. 条件判断 -->
<#if tableComment??>
注释:${tableComment}
<#else>
注释:暂无
</#if>
<#-- 3. 循环遍历 -->
<#list columns as col>
private ${col.javaType} ${col.fieldName};
</#list>
<#-- 4. 内建函数 -->
驼峰命名:${columnName?camel_case}
首字母大写:${className?cap_first}
日期格式:${.now?string("yyyy-MM-dd")}
<#-- 5. 自定义指令(宏) -->
<#macro addField col>
<#if col.comment??>
/** ${col.comment} */
</#if>
private ${col.javaType} ${col.fieldName};
</#macro>
<#-- 6. 命名空间与导入 -->
<#import "/common/utils.ftl" as utils>
二、代码生成器架构设计
2.1 整体架构
flowchart LR
subgraph 输入层
A1[数据库连接] --> A2[表结构读取]
A3[生成配置] --> A4[模板选择]
end
subgraph 处理层
B1[元数据解析] --> B2[类型映射]
B2 --> B3[数据模型构建]
A4 --> B4[模板加载]
B3 --> B5[FreeMarker引擎]
B4 --> B5
B5 --> B6[代码生成]
end
subgraph 输出层
B6 --> C1[Entity.java]
B6 --> C2[DTO.java]
B6 --> C3[Mapper.java]
B6 --> C4[Mapper.xml]
B6 --> C5[Service.java]
B6 --> C6[Controller.java]
B6 --> C7[Vue页面]
end
A2 --> B1
style B5 fill:#4CAF50,color:#fff
2.2 数据模型构建
代码生成器的核心是将数据库元数据转换为模板可用的数据模型:
/**
* 代码生成数据模型
* 封装模板渲染所需的全部数据
*/
@Data
public class GenDataModel {
/** 基础配置 */
private String packageName; // 包名: com.example.system
private String moduleName; // 模块名: system
private String businessName; // 业务名: user
private String functionName; // 功能名: 用户管理
private String author; // 作者
private String datetime; // 生成日期
/** 表信息 */
private String tableName; // 表名: sys_user
private String tableComment; // 表注释: 用户表
private String className; // 类名: SysUser
/** 字段列表 */
private List<ColumnModel> columns;
/** 主键字段 */
private ColumnModel pkColumn;
}
/**
* 字段数据模型
*/
@Data
public class ColumnModel {
private String columnName; // 列名: user_name
private String fieldName; // 属性名: userName
private String javaType; // Java类型: String
private String jdbcType; // JDBC类型: VARCHAR
private String comment; // 注释: 用户名
private boolean isPk; // 是否主键
private boolean isRequired; // 是否必填
private boolean isInsert; // 是否插入字段
private boolean isEdit; // 是否编辑字段
private boolean isList; // 是否列表字段
private boolean isQuery; // 是否查询字段
private String queryType; // 查询方式: EQ/LIKE/BETWEEN
private String htmlType; // 显示类型: input/select/date
private String dictType; // 字典类型
}
2.3 类型映射配置
/**
* 数据库类型与Java类型映射
*/
public class TypeMapper {
private static final Map<String, String> TYPE_MAPPING = Map.ofEntries(
entry("bigint", "Long"),
entry("int", "Integer"),
entry("tinyint", "Integer"),
entry("smallint", "Integer"),
entry("mediumint", "Integer"),
entry("varchar", "String"),
entry("char", "String"),
entry("text", "String"),
entry("longtext", "String"),
entry("datetime", "LocalDateTime"),
entry("date", "LocalDate"),
entry("timestamp", "LocalDateTime"),
entry("decimal", "BigDecimal"),
entry("double", "Double"),
entry("float", "Float")
);
/**
* 将JDBC类型映射为Java类型
*/
public static String toJavaType(String jdbcType) {
return TYPE_MAPPING.getOrDefault(jdbcType.toLowerCase(), "String");
}
}
三、各层模板示例
3.1 Entity模板
package ${packageName}.entity;
<#list importList as imp>
import ${imp};
</#list>
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
/**
* ${functionName}实体类
*
* @author ${author}
* @date ${datetime}
*/
@Data
@TableName("${tableName}")
public class ${className} {
<#list columns as col>
<#if col.isPk>
@TableId(type = IdType.ASSIGN_ID)
</#if>
<#if col.comment??>
/** ${col.comment} */
</#if>
<#if col.fieldName != col.columnName>
@TableField("${col.columnName}")
</#if>
private ${col.javaType} ${col.fieldName};
</#list>
}
3.2 DTO模板
package ${packageName}.dto;
<#list importList as imp>
import ${imp};
</#list>
import lombok.Data;
import jakarta.validation.constraints.*;
/**
* ${functionName}请求DTO
*
* @author ${author}
* @date ${datetime}
*/
@Data
public class ${className}DTO {
<#list columns as col>
<#if col.isInsert || col.isEdit>
<#if col.comment??>
/** ${col.comment} */
</#if>
<#if col.isRequired>
@NotNull(message = "${col.comment}不能为空")
</#if>
<#if col.javaType == "String" && col.isRequired>
@NotBlank(message = "${col.comment}不能为空")
</#if>
private ${col.javaType} ${col.fieldName};
</#if>
</#list>
}
3.3 Mapper模板
package ${packageName}.mapper;
import ${packageName}.entity.${className};
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* ${functionName}Mapper接口
*
* @author ${author}
* @date ${datetime}
*/
@Mapper
public interface ${className}Mapper extends BaseMapper<${className}> {
}
3.4 Mapper XML模板
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${packageName}.mapper.${className}Mapper">
<resultMap id="BaseResultMap" type="${packageName}.entity.${className}">
<#list columns as col>
<result column="${col.columnName}" property="${col.fieldName}" />
</#list>
</resultMap>
</mapper>
3.5 Service模板
package ${packageName}.service;
import ${packageName}.entity.${className};
import ${packageName}.dto.${className}DTO;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* ${functionName}Service接口
*
* @author ${author}
* @date ${datetime}
*/
public interface ${className}Service extends IService<${className}> {
/**
* 分页查询${functionName}列表
*/
Page<${className}VO> selectPage(${className}QueryDTO query);
/**
* 根据ID查询${functionName}详情
*/
${className}VO selectById(Long id);
/**
* 新增${functionName}
*/
void add(${className}DTO dto);
/**
* 修改${functionName}
*/
void update(${className}DTO dto);
/**
* 批量删除${functionName}
*/
void deleteByIds(List<Long> ids);
}
3.6 Controller模板
package ${packageName}.controller;
import ${packageName}.service.${className}Service;
import ${packageName}.dto.${className}DTO;
import ${packageName}.dto.${className}QueryDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* ${functionName}Controller
*
* @author ${author}
* @date ${datetime}
*/
@RestController
@RequestMapping("/${moduleName}/${businessName}")
@RequiredArgsConstructor
public class ${className}Controller {
private final ${className}Service ${className?uncap_first}Service;
/** 分页查询 */
@GetMapping("/page")
public Result<Page<${className}VO>> page(${className}QueryDTO query) {
return Result.success(${className?uncap_first}Service.selectPage(query));
}
/** 查询详情 */
@GetMapping("/{id}")
public Result<${className}VO> getInfo(@PathVariable Long id) {
return Result.success(${className?uncap_first}Service.selectById(id));
}
/** 新增 */
@PostMapping
public Result<Void> add(@RequestBody @Valid ${className}DTO dto) {
${className?uncap_first}Service.add(dto);
return Result.success();
}
/** 修改 */
@PutMapping
public Result<Void> edit(@RequestBody @Valid ${className}DTO dto) {
${className?uncap_first}Service.update(dto);
return Result.success();
}
/** 删除 */
@DeleteMapping("/{ids}")
public Result<Void> remove(@PathVariable List<Long> ids) {
${className?uncap_first}Service.deleteByIds(ids);
return Result.success();
}
}
3.7 Vue页面模板(核心片段)
<template>
<div class="app-container">
<!-- 搜索栏 -->
<el-form :model="queryParams" ref="queryRef" :inline="true">
<#list columns as col>
<#if col.isQuery>
<el-form-item label="${col.comment}" prop="${col.fieldName}">
<#if col.queryType == "LIKE">
<el-input v-model="queryParams.${col.fieldName}" placeholder="请输入${col.comment}" clearable />
<#elseif col.htmlType == "select">
<el-select v-model="queryParams.${col.fieldName}" placeholder="请选择${col.comment}" clearable>
<el-option v-for="dict in ${col.fieldName}Options" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
<#elseif col.htmlType == "datetime">
<el-date-picker v-model="queryParams.${col.fieldName}" type="date" placeholder="选择${col.comment}" />
</#if>
</el-form-item>
</#if>
</#list>
<el-form-item>
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="tableData" v-loading="loading">
<#list columns as col>
<#if col.isList>
<el-table-column label="${col.comment}" prop="${col.fieldName}" />
</#if>
</#list>
<el-table-column label="操作" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleUpdate(scope.row)">修改</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
四、代码生成核心流程
flowchart TD
A[用户选择数据库表] --> B[读取表结构元数据]
B --> C[字段类型映射]
C --> D[构建GenDataModel]
D --> E[加载FreeMarker配置]
E --> F[注册自定义函数]
F --> G{遍历模板列表}
G --> H[Entity.ftl]
G --> I[DTO.ftl]
G --> J[Mapper.ftl]
G --> K[Service.ftl]
G --> L[Controller.ftl]
G --> M[Vue.ftl]
H --> N[模板 + 数据模型渲染]
I --> N
J --> N
K --> N
L --> N
M --> N
N --> O[生成代码文件]
O --> P[打包为ZIP下载]
style D fill:#4CAF50,color:#fff
style N fill:#2196F3,color:#fff
style P fill:#FF9800,color:#fff
生成器核心代码
/**
* 代码生成器核心服务
*/
@Service
@RequiredArgsConstructor
public class GenService {
private final Configuration freemarkerConfig;
/**
* 执行代码生成
* @param genConfig 生成配置
* @return 生成的文件列表
*/
public List<GenFile> generate(GenConfig genConfig) {
// 1. 构建数据模型
GenDataModel dataModel = buildDataModel(genConfig);
// 2. 获取模板列表
List<String> templates = getTemplateList(genConfig.getGenType());
// 3. 逐个模板渲染
List<GenFile> files = new ArrayList<>();
for (String template : templates) {
try {
Template tpl = freemarkerConfig.getTemplate(template);
String content = FreeMarkerTemplateUtils
.processTemplateIntoString(tpl, buildModelMap(dataModel));
String filePath = buildOutputPath(template, dataModel);
files.add(new GenFile(filePath, content));
} catch (Exception e) {
throw new RuntimeException("模板渲染失败: " + template, e);
}
}
return files;
}
/**
* 注册FreeMarker自定义函数
*/
@PostConstruct
public void initCustomFunctions() {
// 驼峰转换函数
freemarkerConfig.setSharedVariable("toCamelCase",
(TemplateMethodModelEx) args -> {
String str = ((SimpleScalar) args.get(0)).getAsString();
return CaseFormat.LOWER_UNDERSCORE.to(
CaseFormat.LOWER_CAMEL, str);
});
}
}
五、高级技巧
5.1 模板继承与复用
<#-- base-entity.ftl: 基础实体宏定义 -->
<#macro entityFields columns>
<#list columns as col>
<#if col.comment??>
/** ${col.comment} */
</#if>
private ${col.javaType} ${col.fieldName};
</#list>
</#macro>
<#macro entityImports columns>
<#assign types = []>
<#list columns as col>
<#if !types?seq_contains(col.javaType)>
import ${col.fullJavaType};
<#assign types = types + [col.javaType]>
</#if>
</#list>
</#macro>
5.2 条件化模板生成
<#-- 根据配置决定是否生成某些代码 -->
<#if genConfig.genTree>
/** 子节点列表 */
@TableField(exist = false)
private List<${className}> children = new ArrayList<>();
</#if>
<#if genConfig.genExcel>
<#list columns as col>
<#if col.isList>
@ExcelProperty("${col.comment}")
private ${col.javaType} ${col.fieldName};
</#if>
</#list>
</#if>
结论与建议
FreeMarker在代码生成中的优势
| 优势 | 说明 |
|---|---|
| 语法简洁 | <#if>、<#list> 等指令直观易学 |
| 类型安全 | 编译期检查模板语法错误 |
| 扩展性强 | 支持自定义函数、宏、命名空间 |
| 性能优异 | 模板缓存机制,渲染速度快 |
| 生态成熟 | Spring Boot官方支持 |
最佳实践建议
- 模板版本管理:将模板文件纳入Git版本控制,支持模板的迭代和回滚
- 自定义函数:封装常用的命名转换、类型判断等逻辑为自定义函数,减少模板复杂度
- 模板预览:生成前提供预览功能,让用户确认生成结果
- 增量生成:支持只生成新增字段对应的代码,避免覆盖已有修改
- 模板校验:生成后自动进行语法校验,确保生成的代码可编译