FreeMarker 模板引擎实现代码生成器:自动生成 Entity/DTO/Controller/Vue 全栈代码

作者:忆笙智云官方 | 发布时间:2026-06-14 08:00 | 更新时间:2026-06-14 08:00

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官方支持

最佳实践建议

  1. 模板版本管理:将模板文件纳入Git版本控制,支持模板的迭代和回滚
  2. 自定义函数:封装常用的命名转换、类型判断等逻辑为自定义函数,减少模板复杂度
  3. 模板预览:生成前提供预览功能,让用户确认生成结果
  4. 增量生成:支持只生成新增字段对应的代码,避免覆盖已有修改
  5. 模板校验:生成后自动进行语法校验,确保生成的代码可编译

相关资源链接