第一章:FreeMarker 基础入门
1.1 什么是 FreeMarker
| 概念名称 | 说明 | 注意事项 |
|---|
| FreeMarker 定义 | FreeMarker 是一个基于 Java 的模板引擎,用于生成文本输出(如 HTML、XML、邮件、配置文件等),通过将模板与数据模型结合实现动态内容渲染。 | 不是 Web 框架,仅负责视图层渲染,需配合 Java 后端使用。 |
| 模板(Template) | 以 .ftl 为扩展名的文本文件,包含静态内容和 FreeMarker 指令/插值,用于定义输出格式。 | 模板本身不含业务逻辑,仅描述”如何展示”。 |
| 数据模型(Data Model) | 由 Java 对象构成的树形结构,供模板访问变量和方法。通常为 Map、POJO 或自定义 TemplateModel。 | 模板只能读取数据,不能修改数据模型。 |
| 输出(Output) | FreeMarker 引擎将模板与数据模型合并后生成的最终文本结果。 | 输出为纯字符串,可写入文件、HTTP 响应等。 |
1.2 FreeMarker 核心特性
| 特性名称 | 说明 | 注意事项 |
|---|
| 纯 Java 实现 | 完全用 Java 编写,可在任何支持 Java 的环境中运行。 | 无本地依赖,跨平台。 |
| 模板与代码分离 | 模板文件独立于 Java 代码,便于前端与后端协作开发。 | 需约定数据模型结构。 |
| 强大的表达式语言 | 支持算术、逻辑、比较、序列操作等表达式,类似简化版编程语言。 | 不支持赋值或副作用操作。 |
| 内建函数(Built-ins) | 提供大量内建函数处理字符串、日期、数字、序列等(如 ?upper_case、?size)。 | 内建函数不可修改,但可扩展。 |
| 国际化支持 | 支持多语言模板,可结合 Java 的 ResourceBundle 实现 i18n。 | 需手动管理语言资源。 |
| 安全模式(sandbox) | 可限制模板访问敏感 Java 方法,防止任意代码执行。 | 默认不启用,生产环境建议配置。 |
| 高性能 | 模板可编译缓存,避免重复解析,适合高并发场景。 | 首次渲染较慢,后续快。 |
1.3 环境搭建与依赖引入
| 步骤名称 | 操作细节 | 注意事项 |
|---|
| Maven 依赖引入 | 在 pom.xml 中添加依赖 | 推荐使用最新稳定版(截至 2026 年为 2.3.33)。 |
| Gradle 依赖引入 | 在 build.gradle 中添加依赖 | 注意版本兼容性。 |
| 模板目录设置 | 创建 src/main/resources/templates 目录存放 .ftl 文件。 | 路径可自定义,但需在 Configuration 中指定。 |
| 配置 FreeMarker 引擎 | 使用 freemarker.template.Configuration 类初始化模板加载器和编码。 | 必须设置 setDirectoryForTemplateLoading() 或 setClassForTemplateLoading()。 |
| 字符编码设置 | 调用 configuration.setDefaultEncoding("UTF-8") | 避免中文乱码,建议统一使用 UTF-8。 |
Maven 依赖:
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.33</version>
</dependency>
Gradle 依赖:
implementation 'org.freemarker:freemarker:2.3.33'
1.4 第一个 FreeMarker 模板示例
| 组件名称 | 代码示例 | 说明 |
|---|
| 模板文件(hello.ftl) | 见下方代码块 | 使用 ${name} 插值语法输出变量。 |
| Java 主程序 | 见下方代码块 | 1. 初始化 Configuration 2. 加载模板 3. 准备数据模型 4. 渲染输出 |
| 运行结果 | 见下方代码块 | 控制台输出渲染后的 HTML。 |
模板文件(hello.ftl):
<html>
<body>
<h1>Hello, ${name}!</h1>
</body>
</html>
Java 主程序:
import freemarker.template.*;
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws Exception {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_33);
cfg.setDirectoryForTemplateLoading(new File("src/main/resources/templates"));
cfg.setDefaultEncoding("UTF-8");
Template template = cfg.getTemplate("hello.ftl");
Map<String, Object> data = new HashMap<>();
data.put("name", "World");
Writer out = new OutputStreamWriter(System.out);
template.process(data, out);
out.flush();
}
}
运行结果:
<html>
<body>
<h1>Hello, World!</h1>
</body>
</html>
第二章:模板语法详解
2.1 插值(Interpolation)
| 方法/语法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 基本插值 | ${expression} | 将表达式的字符串表示插入输出 | ${user.name} | 若 user.name 为 null,默认抛异常(可配置) |
| 禁用转义插值 | ${expression?no_esc} | 输出原始内容(不进行 HTML/XML 转义) | ${htmlContent?no_esc} | 存在 XSS 风险,慎用 |
| 数字格式化插值 | ${number?string("0.##")} | 按指定格式输出数字 | ${price?string("#,##0.00")} | 依赖 locale 设置 |
| 日期格式化插值 | ${date?string("yyyy-MM-dd")} | 按指定格式输出日期 | ${now?string("HH:mm:ss")} | now 需为 java.util.Date 或兼容类型 |
| 安全插值(缺失处理) | ${expression!} | 表达式缺失时输出空字符串 | ${user.nickname!} | 等价于 ${user.nickname!""} |
| 自定义默认值插值 | ${expression!"Guest"} | 表达式缺失时使用指定默认值 | ${title!"Untitled"} | 支持任意表达式作为默认值 |
2.2 指令(Directives)
| 指令名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| assign | <#assign name=value> | 在模板中定义变量 | <#assign user="Alice">Hello ${user}! | 变量作用域为当前命名空间 |
| if / else / elseif | <#if cond>...<#elseif cond>...<#else>... | 条件判断 | <#if age >= 18>Adult<#else>Minor</#if> | 支持嵌套 |
| list | <#list seq as item>...<#else>...</#list> | 遍历序列或集合 | <#list users as u>${u.name}<#if u_has_next>, </#if></#list> | 自动提供 _index、_has_next 等循环变量 |
| include | <#include "template.ftl"> | 包含另一个模板文件 | <#include "header.ftl"> | 路径相对于当前模板 |
| import | <#import "lib.ftl" as lib> | 导入宏库并创建命名空间 | <#import "/common/macros.ftl" as m><@m.alert message="Hi"/> | 宏需在被导入模板中定义 |
| macro | <#macro name args>...<#nested>...</#macro> | 定义可复用的宏 | <#macro greet name>Hello ${name}!</#macro><@greet name="Bob"/> | 支持可选参数和嵌套内容(<#nested>) |
| setting | <#setting name=value> | 临时修改配置(如 number_format) | <#setting number_format="0.##"> | 仅影响当前模板后续部分 |
| noparse | <#noparse>...</#noparse> | 禁止解析其中内容(用于展示模板代码) | <#noparse>${name}</#noparse> → 输出 ${name} | 调试或文档编写时有用 |
2.3 表达式(Expressions)
| 表达式类型 | 语法示例 | 说明 | 注意事项 |
|---|
| 字符串字面量 | "Hello" 或 'Hello' | 单双引号均可,支持转义(如 \n、\") | 不支持多行字符串 |
| 数字字面量 | 42、3.14、-1.2e5 | 支持整数、浮点、科学计数法 | 所有数字在 FreeMarker 中为 BigDecimal 或 Double |
| 布尔字面量 | true、false | 逻辑值 | 区分大小写 |
| 变量引用 | user、user.name、users[0] | 访问数据模型中的变量 | 若中间节点为 null,会抛异常(除非用 !) |
| 序列(List) | ["a", "b", "c"] | 创建内联序列 | 元素类型可混合 |
| 哈希(Map) | {"name": "Alice", "age": 30} | 创建内联哈希 | 键必须是字符串 |
| 算术运算 | a + b、x * y - z / 2 | 支持 +、-、*、/、% | 除法结果为精确小数(非整数除) |
| 比较运算 | x == y、a < b、name != "admin" | 支持 ==、!=、<、>、<=、>= | 字符串比较区分大小写 |
| 逻辑运算 | cond1 && cond2、!isEmpty | 支持 &&、||、! | 短路求值 |
| 内建函数调用 | name?upper_case、list?size | 对值进行转换或查询 | 所有内建函数以 ? 开头 |
| 方法调用 | obj.method(arg) | 调用 Java 对象的方法(若允许) | 默认禁止调用任意方法(安全限制) |
2.4 注释与空白处理
| 功能名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 模板注释 | <#-- This is a comment --> | 在模板中添加注释,不会出现在输出中 | <#-- 用户欢迎信息 -->Hello ${name}! | 支持多行 |
| 输出注释(HTML风格) | <!-- Visible in output --> | 生成可见的 HTML/XML 注释 | <!-- Generated by FreeMarker --> | 会出现在最终输出中 |
| 压缩空白(strip_whitespace) | <#ftl strip_whitespace=true> | 自动移除模板中多余的空白和换行 | 放在模板顶部 | 可能影响 <pre> 等格式化文本 |
| 手动空白抑制 | 通过注释或指令控制换行 | 使用 <#t>(trim)、<#rt>(right trim)等 | 高级用法,较少使用 | — |
| 空白保留 | 默认行为 | 模板中的空白字符原样输出 | 若需精确控制布局,避免自动压缩 | 建议开发阶段关闭 strip_whitespace |
第三章:数据模型与变量
3.1 变量定义与作用域
| 概念/操作名称 | 说明 | 代码示例 | 注意事项 |
|---|
| 模板变量(Template-scoped) | 使用 <#assign> 定义,仅在当前模板中有效 | <#assign count = 10> | 重新赋值会覆盖原值 |
| 全局变量(Global) | 使用 <#global> 定义,对所有模板可见(包括 include/import 的模板) | <#global siteName = "MySite"> | 慎用,易造成命名冲突 |
| 局部变量(Local) | 在宏或函数内部使用 <#local> 定义,仅在当前宏/函数内有效 | <#macro test><#local x=1>${x}</#macro> | 避免与模板变量同名 |
| 数据模型变量 | 由 Java 代码传入的根变量(如 Map 中的 key) | Java: data.put("user", userObj); → 模板中 ${user.name} | 模板无法修改此变量 |
| 命名空间变量 | 通过 <#import> 导入的宏库中的变量 | <#import "lib.ftl" as lib> → 访问 ${lib.version} | 各命名空间独立 |
| 变量作用域优先级 | Local > Template > Global > Data Model | 若多个作用域存在同名变量,按此顺序查找 | 建议避免同名 |
3.2 内建函数(Built-ins)
注:以下列出常用内建函数,格式为 value?builtin(args)
| 内建函数名称 | 语法示例 | 用途 | 代码示例 | 注意事项 |
|---|
| string | num?string("0.##") | 格式化数字或日期为字符串 | ${price?string("#,##0.00")} | 依赖 locale |
| upper_case / lower_case | name?upper_case | 转换字符串大小写 | ${"Hello"?lower_case} → hello | 仅适用于字符串 |
| cap_first | word?cap_first | 首字母大写 | "hello"?cap_first → “Hello” | 仅处理第一个字符 |
| html | text?html | HTML 转义(< → < 等) | ${userInput?html} | 默认插值已自动转义(若配置了 auto_esc) |
| url | param?url | URL 编码 | ${query?url} | 用于拼接 URL 参数 |
| size | list?size、str?size | 获取序列、哈希或字符串长度 | <#if users?size > 0>... | 哈希返回键数量 |
| keys / values | map?keys、map?values | 获取哈希的键或值列表 | <#list userMap?keys as k>${k}=${userMap[k]}</#list> | 顺序不确定 |
| sort / sort_by | users?sort_by("name") | 对序列排序 | <#list products?sort_by("price") as p>... | 支持嵌套属性(如 "address.city") |
| split | text?split(",") | 按分隔符拆分字符串为序列 | <#assign parts = "a,b,c"?split(",")> | 返回序列 |
| replace | text?replace("old", "new") | 字符串替换 | "abc"?replace("b", "X") → “aXc” | 支持正则 |
| exists | variable?exists | 判断变量是否存在(不推荐,建议用 ??) | <#if user.name?exists>... | 已废弃,优先使用 ?? |
| has_content | str?has_content | 判断字符串/序列/哈希是否非空 | <#if list?has_content>... | 比 ?size > 0 更安全(防 null) |
3.3 哈希表、序列与集合操作
| 操作类型 | 语法/方法 | 用途 | 代码示例 | 注意事项 |
|---|
| 访问哈希值 | map.key 或 map["key"] | 获取哈希中指定键的值 | ${user["name"]} 或 ${user.name} | 推荐用点号,除非 key 含特殊字符 |
| 序列索引访问 | seq[index] | 获取序列中指定位置元素(从 0 开始) | ${users[0].name} | 越界抛异常 |
| 序列切片 | seq[from..to] | 截取子序列(包含 to) | ${letters[1..3]} → [b, c, d] | from 和 to 可为表达式 |
| 序列连接 | seq1 + seq2 | 合并两个序列 | <#assign all = users + admins> | 返回新序列 |
| 哈希合并 | hash1 + hash2 | 合并两个哈希(后者覆盖前者同名键) | <#assign config = defaults + overrides> | 仅浅合并 |
| 创建序列 | ["a", "b"] | 内联定义序列 | <#list ["Mon", "Tue"] as day>${day}</#list> | 元素可为任意类型 |
| 创建哈希 | {"x": 1, "y": 2} | 内联定义哈希 | <#assign point = {"x": 10, "y": 20}> | 键必须是字符串字面量 |
| 检查键是否存在 | key in hash | 判断哈希是否包含某键 | <#if "email" in user>... | 安全,不抛异常 |
| 遍历哈希 | <#list hash?keys as k>...hash[k]... | 遍历哈希的键值对 | <#list settings?keys as k>${k}=${settings[k]}</#list> | 无固定顺序 |
3.4 处理 null 与缺失值
| 处理方式 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 默认值操作符 | expr!default | 表达式缺失或 null 时使用默认值 | ${user.nickname!"Guest"} | default 可为任意表达式 |
| 空默认值 | expr! | 等价于 expr!"" | ${title!} | 常用于防止异常 |
| 存在性检查 | expr?? | 判断表达式是否定义且非 null | <#if user.phone??>Phone: ${user.phone}</#if> | 推荐替代 ?exists |
| 安全链式访问 | a.b.c! | 中间任一节点为 null 时返回空 | ${company.ceo.name!"N/A"} | 避免 NullPointerException |
| 配置缺失值处理 | cfg.setTemplateExceptionHandler(...) | 全局设置缺失变量行为 | cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER) | 默认抛异常,开发时可用 DEBUG_HANDLER 打印警告 |
| 条件渲染 | 结合 ?? 与 #if | 仅当变量存在时渲染 | <#if content??>${content}</#if> | 比 ! 更灵活(可包含多行) |
第四章:控制结构与逻辑
4.1 条件判断(#if / #else / #elseif)
| 指令/语法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 基本 if | <#if condition>...<#else>...</#if> | 根据布尔表达式执行分支 | <#if user.isAdmin>Admin<#else>User</#if> | condition 可为任意表达式(非 null 且非 false 视为真) |
| elseif | <#if c1>...<#elseif c2>...<#else>...</#if> | 多条件分支 | <#if score >= 90>A<#elseif score >= 80>B<#else>C</#if> | 支持多个 #elseif |
| 嵌套 if | 在 #if 块内再使用 #if | 实现复杂逻辑 | <#if loggedIn><#if premium>Welcome!</#if></#if> | 注意缩进可读性 |
| 空值安全判断 | <#if var??>...<#else>...</#if> | 先检查变量是否存在 | <#if email??>Contact: ${email}</#if> | 避免因 null 抛异常 |
| 布尔表达式组合 | 使用 &&、||、! | 构建复合条件 | <#if age >= 18 && hasLicense> | 短路求值 |
| switch 替代方案 | 使用多个 #elseif | FreeMarker 无原生 #switch | <#if type == "A">...<#elseif type == "B">...<#else>...</#if> | 对于大量分支,建议在 Java 层处理 |
4.2 循环结构(#list / #items / #break)
| 指令/语法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 基本 list | <#list sequence as item>...<#else>...</#list> | 遍历序列或集合 | <#list products as p>${p.name}</#list> | 若序列为空,执行 #else 块 |
| 循环变量 | _index、_size、_has_next、_is_first、_is_last | 获取循环状态信息 | <#list users as u>${u_index + 1}. ${u.name}<#if u_has_next>, </#if></#list> | 所有变量前缀为 item_(如 u_index) |
| items(已废弃) | <#items sequence as item>... | 旧版遍历语法(FreeMarker 2.3+ 不推荐) | — | 应使用 #list 替代 |
| break | <#break> | 提前退出当前循环 | <#list items as i><#if i > 10><#break></#if>${i}</#list> | 仅在 #list 内有效 |
| 嵌套循环 | 在 #list 内再使用 #list | 处理二维数据 | <#list matrix as row><#list row as cell>${cell}</#list></#list> | 内外层循环变量名需区分 |
| 遍历哈希 | <#list hash?keys as k>...hash[k]... | 遍历哈希的键值对 | <#list config?keys as key>${key}=${config[key]}</#list> | 顺序不确定 |
| 空序列处理 | 使用 #else 分支 | 序列为空时提供默认内容 | <#list orders as o>...<#else>No orders.</#list> | 比先判断 ?size 更简洁 |
4.3 宏(Macro)与函数(Function)
| 特性名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 定义宏 | <#macro name [args]>...<#nested>...</#macro> | 创建可复用模板片段 | <#macro alert msg><div class="alert">${msg}</div></#macro> | 宏不返回值,直接输出 |
| 调用宏 | <@macroName arg=value /> 或 <@macroName>...</@macroName> | 使用宏 | <@alert msg="Hello"/> 或 <@alert>"Hi"</@alert> | 有嵌套内容时用结束标签 |
| 宏参数 | 支持位置参数和命名参数 | 传递数据给宏 | <#macro greet name title="Mr.">${title} ${name}</#macro><@greet name="Smith" title="Dr."/> | 参数可设默认值 |
| 嵌套内容(#nested) | 在宏定义中使用 <#nested> | 插入调用时传入的内容 | <#macro panel><div><#nested></div></#macro><@panel>Content</@panel> | 类似 React 的 children |
| 定义函数 | <#function name [args]>...<#return value>...</#function> | 创建返回值的可复用逻辑 | <#function square x><#return x * x></#function> | 函数必须 #return,不能直接输出 |
| 调用函数 | ${functionName(args)} | 获取函数返回值 | ${square(5)} → 25 | 用于表达式上下文 |
| 函数 vs 宏 | 函数返回值,宏输出文本 | 根据需求选择 | 函数用于计算,宏用于生成 HTML 片段 | 不能混用调用方式 |
4.4 嵌套与包含(#nested / #include)
| 指令/特性名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| include | <#include "template.ftl"> | 将另一个模板文件内容插入当前位置 | <#include "header.ftl"> | 路径相对于当前模板;被包含模板可访问当前作用域变量 |
| include with params | <#include "tpl.ftl" encoding="UTF-8"> | 指定编码或传递局部变量(通过哈希) | <#include "userCard.ftl" parse=true> | parse 控制是否解析内容(默认 true) |
| import | <#import "macros.ftl" as lib> | 导入宏库,创建独立命名空间 | <#import "/common/utils.ftl" as u><@u.button label="OK"/> | 被导入模板中的 #assign 变量也进入该命名空间 |
| nested(在宏中) | <#nested> | 在宏定义中插入调用者提供的内容 | 见 4.3 示例 | 只能在宏内部使用 |
| 动态包含 | <#include templateName> | 模板名可为变量 | <#assign layout = isAdmin?then("admin.ftl", "user.ftl")><#include layout> | 需确保变量值是合法模板路径 |
| 包含 vs 宏 | include 复用模板文件,macro 复用逻辑片段 | include 适合完整组件,macro 适合小部件 | header/footer 用 include,button/alert 用 macro | include 会增加文件 I/O(但可缓存) |
第五章:模板复用与模块化
5.1 include 指令
| 操作名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 基本包含 | <#include "path/to/template.ftl"> | 将指定模板内容插入当前位置 | <#include "header.ftl"> | 路径相对于当前模板所在目录 |
| 动态路径包含 | <#include templateName> | 模板路径由变量决定 | <#assign layout = user.role + ".ftl"><#include layout> | 变量值必须是字符串,且路径有效 |
| 指定编码包含 | <#include "tpl.ftl" encoding="UTF-8"> | 显式设置被包含模板的字符编码 | <#include "zh_CN/footer.ftl" encoding="GBK"> | 若未指定,使用 Configuration 的默认编码 |
| 控制解析行为 | <#include "raw.txt" parse=false> | 禁止解析被包含文件中的 FreeMarker 语法 | 用于包含纯文本、JS、CSS 等 | parse=true 为默认值 |
| 传递局部变量 | <#include "tpl.ftl" [variables]> | FreeMarker 不直接支持 include 传参 | 需通过全局/模板变量间接传递,或改用宏 | 推荐使用宏替代需传参的 include |
| 错误处理 | 被包含模板不存在时抛 TemplateNotFoundException | 安全做法:先检查或使用 try-catch(Java 层) | — | 生产环境应确保模板存在 |
5.2 import 与 macro 库
| 操作名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 导入宏库 | <#import "macros.ftl" as lib> | 加载外部模板中的宏并创建命名空间 | <#import "/common/ui.ftl" as ui> | 路径规则同 include |
| 调用导入的宏 | <@lib.macroName args /> | 使用命名空间中的宏 | <@ui.button label="Submit" type="primary"/> | 必须通过命名空间前缀访问 |
| 宏库定义示例 | 在 macros.ftl 中定义宏 | 提供可复用 UI 组件 | 宏可含默认参数、嵌套内容等 | 宏库文件不应直接渲染,仅作定义 |
| 多次导入同一文件 | 多次 <#import> 同一路径 | FreeMarker 会缓存模板,不会重复加载 | 性能无影响 | 所有导入共享同一模板实例 |
| 导入与作用域 | 导入后,宏库中的 #assign 变量也进入该命名空间 | 实现配置或常量共享 | macros.ftl: <#assign VERSION = "1.0"> → 使用:${ui.VERSION} | 命名空间隔离,避免污染全局 |
| 导出函数 | 宏库中也可定义 #function | 提供计算型工具 | <#function formatDate d><#return d?string("yyyy-MM-dd")></#function> | 通过 ${lib.formatDate(date)} 调用 |
5.3 使用命名空间(#assign in namespace)
| 操作名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 显式指定命名空间赋值 | <#assign x = 1 in ns> | 将变量赋值到指定命名空间 | <#import "lib.ftl" as mylib><#assign config = {...} in mylib> | ns 必须是已存在的命名空间对象 |
| 获取命名空间对象 | 通过 <#import> 返回的别名 | 命名空间即 import 时的别名变量 | mylib 即为命名空间对象 | 不能直接创建空命名空间 |
| 读取命名空间变量 | ${ns.varName} | 访问指定命名空间中的变量 | ${mylib.config.theme} | 若变量不存在,返回 null |
| 动态命名空间操作 | 不支持动态创建命名空间 | 命名空间必须通过 import 静态引入 | — | 无法在运行时生成新命名空间 |
| 全局命名空间 | <#global> 定义的变量属于全局命名空间 | 所有模板共享 | <#global SITE_TITLE = "MyApp"> | 避免滥用,防止冲突 |
| 命名空间 vs 作用域 | 命名空间是显式隔离的容器,作用域是隐式层级 | import 创建命名空间,assign 创建作用域变量 | 命名空间更适用于模块化,作用域适用于临时变量 | 推荐模块间通信使用命名空间 |
第六章:配置与高级功能
6.1 Configuration 类配置项
| 配置项名称 | 方法/属性 | 用途 | 代码示例 | 注意事项 |
|---|
| 模板加载目录 | setDirectoryForTemplateLoading(File dir) | 设置从文件系统加载模板的根目录 | cfg.setDirectoryForTemplateLoading(new File("templates")); | 适用于开发环境 |
| 类路径模板加载 | setClassForTemplateLoading(Class clz, String basePackagePath) | 从 classpath 加载模板 | cfg.setClassForTemplateLoading(App.class, "/templates"); | 推荐用于打包部署(JAR/WAR) |
| 默认编码 | setDefaultEncoding(String encoding) | 设置模板文件和输出的默认字符编码 | cfg.setDefaultEncoding("UTF-8"); | 必须与模板文件实际编码一致 |
| 模板异常处理器 | setTemplateExceptionHandler(TemplateExceptionHandler eh) | 控制模板渲染出错时的行为 | cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); | 可选:DEBUG_HANDLER(打印警告)、IGNORE_HANDLER |
| 模板缓存策略 | setTemplateUpdateDelaySeconds(int delay) | 设置模板缓存过期时间(秒) | cfg.setTemplateUpdateDelaySeconds(5); // 开发时设为低值 | 生产环境建议设为高值或 -1(永久缓存) |
| 自动转义 HTML | setAutoEscapingPolicy(AutoEscapingPolicy policy) | 全局启用 HTML 转义 | cfg.setAutoEscapingPolicy(Configuration.ENABLE_IF_DEFAULT_ENCODING_IS_SAME); | 需配合 outputFormat = “HTML” 使用 |
| 输出格式(Output Format) | setOutputFormat(OutputFormat format) | 指定输出类型(HTML/XML/PlainText 等) | cfg.setOutputFormat(HTMLOutputFormat.INSTANCE); | 影响 ${} 插值是否自动转义 |
| 对象包装器(ObjectWrapper) | setObjectWrapper(ObjectWrapper wrapper) | 控制 Java 对象如何暴露给模板 | cfg.setObjectWrapper(new DefaultObjectWrapper(Configuration.VERSION_2_3_33)); | 可自定义以限制方法调用 |
| 兼容模式 | 构造时指定 Configuration(VERSION_X_Y_Z) | 保持向后兼容旧版语法 | new Configuration(Configuration.VERSION_2_3_33) | 强烈建议显式指定版本 |
6.2 自定义指令与方法
| 扩展类型 | 实现方式 | 用途 | 代码示例 | 注意事项 |
|---|
| 自定义指令(TemplateDirectiveModel) | 实现 TemplateDirectiveModel 接口 | 创建类似 #list 的新指令 | 见下方”自定义指令示例” | 需注册到数据模型或 Configuration |
| 自定义方法(TemplateMethodModelEx) | 实现 TemplateMethodModelEx 接口 | 创建可在表达式中调用的方法 | ${myUtils.formatDate(date)} | 返回值需为 TemplateModel 类型 |
| 注册全局共享变量 | cfg.setSharedVariable("name", model) | 使自定义指令/方法在所有模板中可用 | cfg.setSharedVariable("now", new NowMethod()); | 在初始化 Configuration 时设置 |
| 安全限制 | 默认禁止任意 Java 方法调用 | 防止模板执行危险操作 | 如需开放特定方法,需自定义 ObjectWrapper | 生产环境务必限制 |
自定义指令示例:
public class NowDirective implements TemplateDirectiveModel {
public void execute(Environment env, Map params,
TemplateModel[] loopVars, TemplateDirectiveBody body)
throws TemplateException, IOException {
env.getOut().write(new Date().toString());
}
}
模板中使用:<@now />
自定义方法示例:
public class UpperMethod implements TemplateMethodModelEx {
public Object exec(List args) throws TemplateModelException {
String s = (String) ((SimpleScalar) args.get(0)).getAsString();
return new SimpleScalar(s.toUpperCase());
}
}
模板中使用:${upper("hello")} → “HELLO”
6.3 模板异常处理
| 异常处理方式 | 说明 | 配置/使用方式 | 示例场景 | 注意事项 |
|---|
| RETHROW_HANDLER | 渲染时抛出 TemplateException | cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); | 开发阶段快速定位错误 | 默认行为(某些版本) |
| DEBUG_HANDLER | 打印错误信息到输出流,并继续渲染 | cfg.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER); | 调试时查看缺失变量等警告 | 输出含错误标记(如 [ERROR: ...]) |
| IGNORE_HANDLER | 忽略异常,输出为空 | cfg.setTemplateExceptionHandler(TemplateExceptionHandler.IGNORE_HANDLER); | 容忍部分模板错误(不推荐) | 可能掩盖严重问题 |
| 自定义异常处理器 | 实现 TemplateExceptionHandler 接口 | cfg.setTemplateExceptionHandler(myHandler); | 记录日志、返回友好提示等 | handleTemplateException 方法中可控制输出 |
| 常见异常类型 | TemplateNotFoundException、InvalidReferenceException、NonHashException 等 | — | 文件不存在、访问 null 对象属性、对非哈希使用 .key 等 | 可通过异常类型做精细化处理 |
| 模板语法错误 | 在 getTemplate() 时抛出 ParseException | 需在加载模板时 try-catch | 模板语法错误(如未闭合标签) | 与运行时异常(TemplateException)不同 |
6.4 多语言与国际化支持
| 功能名称 | 实现方式 | 用途 | 代码示例 | 注意事项 |
|---|
| 使用 ResourceBundle | Java 层加载 messages_zh_CN.properties 等 | 提供多语言键值对 | Java: ResourceBundle bundle = ResourceBundle.getBundle("messages", locale); 传入模板:data.put("msg", bundle); | 模板中通过 ${msg["welcome"]} 访问 |
| 模板内切换语言 | 传入不同 locale 对应的 ResourceBundle | 同一模板支持多语言 | <#if lang == "en">${msg_en["title"]}<#else>${msg_zh["title"]}</#if> | 需预先加载所有语言包 |
| 格式化消息(带参数) | 使用 ?string 或 Java 层预处理 | 支持占位符(如 "Hello {0}") | Java: MessageFormat.format(bundle.getString("greet"), name) | FreeMarker 本身无内置 MessageFormat 支持 |
| 日期/数字本地化 | 依赖 Java 的 Locale 和 Configuration 设置 | 按用户区域格式化数值 | ${date?string.short} → 根据 locale 输出不同格式 | 需在 Configuration 中设置 setLocale(Locale.CHINA) |
| 模板文件按语言分离 | templates/en/home.ftl、templates/zh/home.ftl | 不同语言使用不同模板文件 | 根据 locale 动态选择模板路径:cfg.getTemplate("home_" + lang + ".ftl"); | 适合内容结构差异大的场景 |
| 编码统一 | 所有 .properties 文件使用 UTF-8(Java 9+) | 避免中文乱码 | Maven 插件可自动转码 | 推荐使用 UTF-8 并设置 -Dfile.encoding=UTF-8 |
第七章:与 Java 集成开发
7.1 Java 对象传入模板
| 传入方式 | 说明 | 代码示例 | 注意事项 |
|---|
| Map 数据模型 | 使用 Map<String, Object> 作为根对象 | Map<String, Object> data = new HashMap<>(); data.put("user", userObj); template.process(data, out); | 最常用方式,结构清晰 |
| POJO 对象 | 直接传入 Java Bean | data.put("product", new Product("Laptop", 5999)); → 模板中 ${product.name} | 需有 public getter 方法(如 getName()) |
| List / 数组 | 传入集合用于遍历 | data.put("items", Arrays.asList("A", "B")); → <#list items as i>${i}</#list> | 支持任意 Iterable 或数组 |
| 基本类型 | String、Number、Boolean 等 | data.put("count", 10); → ${count} | 自动转换为 FreeMarker 内部类型 |
| null 值处理 | Java 中的 null 在模板中视为”缺失” | 若 user.nickname == null,则 ${user.nickname!} 输出空 | 默认访问 null 属性会抛 InvalidReferenceException |
| 静态方法/字段访问 | 默认禁止(安全限制) | 需自定义 ObjectWrapper 才能启用 | 生产环境不建议开放,有安全风险 |
| 日期类型 | 推荐使用 java.util.Date 或 java.time(需适配) | data.put("now", new Date()); → ${now?string("yyyy-MM-dd")} | java.time 类型需 FreeMarker 2.3.27+ 并配置支持 |
7.2 TemplateModel 接口扩展
| 接口/类名称 | 用途 | 实现要点 | 代码示例片段 | 注意事项 |
|---|
TemplateModel | 所有 FreeMarker 模型对象的基接口 | 通常不直接实现,而是使用子接口 | — | 标记接口 |
TemplateScalarModel | 表示字符串值 | 实现 getAsString() 方法 | public class SafeString implements TemplateScalarModel { private String value; public String getAsString() { return value; } } | 用于封装特殊字符串(如已转义内容) |
TemplateNumberModel | 表示数值 | 实现 getAsNumber() | 返回 BigDecimal、Integer 等 | 用于高精度计算场景 |
TemplateHashModel | 表示哈希表(类似 Map) | 实现 get(String key) 和 keys() | 可包装数据库记录、JSON 对象等 | 支持在模板中用 .key 访问 |
TemplateSequenceModel | 表示序列(类似 List) | 实现 size() 和 get(int index) | 适用于大数据集(可实现懒加载) | 避免一次性加载全部数据 |
AdapterTemplateModel | 通用适配器(推荐) | 继承 BeanModel 或使用 DefaultObjectWrapper 自动包装 | 通常无需手动实现 | FreeMarker 默认使用 DefaultObjectWrapper 自动转换 Java 对象 |
| 自定义包装器 | 控制哪些方法/属性暴露给模板 | 继承 DefaultObjectWrapper 并重写 handleUnknownProperty 等 | 可禁止访问 getClass()、wait() 等危险方法 | 提升安全性 |
7.3 Spring Boot 中集成 FreeMarker
| 配置项/操作 | 说明 | 配置方式 | 代码示例 | 注意事项 |
|---|
| 引入 starter | 添加 FreeMarker Spring Boot Starter | Maven 依赖 | 见下方代码块 | 自动配置 FreeMarkerConfigurer |
| 模板路径 | 默认 classpath:/templates/ | spring.freemarker.template-loader-path: classpath:/views/ | 模板文件放 src/main/resources/views/ | 路径必须以 / 开头 |
| 后缀设置 | 默认 .ftl | spring.freemarker.suffix: .html | 模板文件可命名为 home.html | — |
| 编码设置 | 默认 UTF-8 | spring.freemarker.charset: UTF-8 | — | 通常无需修改 |
| 缓存控制 | 开发时禁用缓存 | spring.freemarker.cache: false | 修改模板后刷新浏览器生效 | 生产环境应设为 true |
| Controller 返回 | 返回逻辑视图名 | — | 见下方代码块 | Spring 自动将 Model 数据传入模板 |
| 全局变量注入 | 通过 FreeMarkerConfigurer 添加共享变量 | Java Config | 见下方代码块 | 适用于全局常量 |
Maven 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
Controller 示例:
@Controller
public class HomeController {
@GetMapping("/home")
public String home(Model model) {
model.addAttribute("message", "Hello");
return "home"; // 对应 home.ftl
}
}
全局变量注入:
@Bean
public FreeMarkerConfigurer freemarkerConfig() {
FreeMarkerConfigurer config = new FreeMarkerConfigurer();
config.setTemplateLoaderPath("classpath:/templates/");
config.setFreemarkerVariables(Map.of("appVersion", "1.0"));
return config;
}
模板中可直接使用:${appVersion}
7.4 缓存与性能优化
| 优化策略 | 说明 | 实施方式 | 注意事项 |
|---|
| 模板缓存 | Configuration 缓存已解析的 Template 对象 | 默认启用;通过 setTemplateUpdateDelaySeconds(-1) 永久缓存 | 生产环境必须开启,避免重复解析 |
| 避免重复创建 Configuration | 单例模式管理 Configuration 实例 | 应用启动时初始化一次,全局复用 | 多线程安全 |
| 数据模型优化 | 传入轻量级 DTO 而非完整 Entity | 减少模板中不必要的对象遍历和方法调用 | 避免在模板中执行复杂业务逻辑 |
| 减少内建函数滥用 | 如 ?sort_by 对大列表排序 | 在 Java 层预排序后再传入 | 模板层计算效率低于 Java |
| 合理使用 include/import | 避免过度拆分模板导致 I/O 增加 | 将高频小部件转为 macro,减少文件包含 | include 有轻微 I/O 开销(但可缓存) |
| 输出流优化 | 使用高效 Writer(如 StringWriter + 缓冲) | StringWriter writer = new StringWriter(1024); template.process(data, writer); | 避免直接使用 System.out 或未缓冲流 |
| 禁用调试功能 | 关闭 DEBUG_HANDLER 和日志 | 生产环境设置 setTemplateExceptionHandler(RETHROW_HANDLER) | 调试信息影响性能 |
| 监控渲染耗时 | 在 process() 前后打点 | 结合 APM 工具(如 SkyWalking)监控慢模板 | 定位性能瓶颈 |
第八章:调试与最佳实践
8.1 模板调试技巧
| 调试方法 | 操作细节 | 代码/配置示例 | 注意事项 |
|---|
| 使用 DEBUG_HANDLER | 渲染时输出错误占位符而非中断 | cfg.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER); | 输出如 [ERROR: Expression user.name is undefined],便于定位缺失变量 |
| 打印变量结构 | 使用内建函数查看数据模型 | ${.data_model?keys} 或 <#list .data_model?keys as k>${k}=${.data_model[k]?string}</#list> | .data_model 表示根哈希;注意循环引用可能造成死循环 |
| 条件输出调试信息 | 在开发环境临时输出变量值 | <#if debugMode>${user?string}</#if> | 上线前应移除或通过配置开关控制 |
| 模板行号提示 | FreeMarker 异常自动包含模板文件和行号 | InvalidReferenceException: ... in template "user.ftl" at line 12 | 确保模板路径正确,避免混淆同名文件 |
| 使用 noparse 展示代码 | 在文档中展示模板语法而不解析 | <#noparse>${name?upper_case}</#noparse> → 输出 ${name?upper_case} | 适用于编写模板使用说明 |
| 单元测试模板 | 编写 Java 测试用例验证模板输出 | 见下方代码块 | 提高模板可靠性,防止回归 |
单元测试示例:
@Test
void testUserTemplate() {
Map<String, Object> data = Map.of("name", "Alice");
String output = renderTemplate("hello.ftl", data);
assertTrue(output.contains("Hello, Alice!"));
}
8.2 安全性注意事项
| 安全风险 | 说明 | 防护措施 | 注意事项 |
|---|
| XSS 攻击 | 用户输入未转义直接输出 | 启用自动 HTML 转义:cfg.setOutputFormat(HTMLOutputFormat.INSTANCE); 或手动使用 ${input?html} | 默认不自动转义,必须显式配置 |
| 任意 Java 方法调用 | 模板可调用对象的 public 方法(如 getClass().getClassLoader()) | 使用安全的 ObjectWrapper,设置 setExposureLevel(EXPOSE_NOTHING) | EXPOSE_NOTHING 禁止所有方法调用 |
| 模板注入 | 动态包含用户可控的模板路径 | 严格校验 include 的路径参数,禁止使用用户输入作为模板名 | 如 <#include userInput> 极度危险 |
| 敏感信息泄露 | 模板中意外输出系统属性或内部对象 | 避免传入含敏感字段的对象;使用 DTO 过滤 | 如数据库连接、密钥等绝不传入模板 |
| DoS 攻击(资源耗尽) | 模板含无限循环或超大集合遍历 | 在 Java 层限制数据规模;避免在模板中处理大数据 | FreeMarker 无内置超时机制 |
| 文件读取漏洞 | 若自定义指令支持读取任意文件 | 自定义指令必须做路径白名单或沙箱限制 | 默认 FreeMarker 无法读取文件 |
8.3 性能调优建议
| 优化方向 | 具体建议 | 实施方式 | 注意事项 |
|---|
| 启用模板缓存 | 避免重复解析相同模板 | cfg.setTemplateUpdateDelaySeconds(-1); // 永久缓存 | 开发环境可设为 0 或低值便于热更新 |
| 减少模板嵌套层级 | 避免过多 #include 和深层宏调用 | 合并小型模板,将逻辑上紧密的片段内联 | 每次 include 有轻微开销(即使缓存) |
| 预处理数据 | 在 Java 层完成排序、过滤、格式化 | 不在模板中使用 ?sort_by、?group_by 等复杂操作 | 模板引擎非计算引擎,Java 处理更快 |
| 使用高效数据结构 | 传入 List 而非 LinkedList(若需随机访问) | 确保序列支持快速索引 | FreeMarker 遍历依赖 TemplateSequenceModel 实现 |
| 避免重复计算 | 将复杂表达式结果赋值给变量复用 | <#assign formattedDate = date?string("yyyy-MM-dd")>${formattedDate} ${formattedDate} | 减少内建函数重复调用 |
| 控制输出缓冲 | 使用带缓冲的 Writer | new BufferedWriter(new OutputStreamWriter(out, "UTF-8")) | 避免频繁 I/O |
| 监控慢模板 | 记录渲染耗时超过阈值的模板 | AOP 或拦截器包装 template.process() | 定位性能瓶颈 |
8.4 常见错误与解决方案
| 错误现象 | 可能原因 | 解决方案 | 注意事项 |
|---|
Expression xxx is undefined | 模板中引用了不存在的变量或属性 | 1. 检查 Java 传入的数据模型是否包含该变量 2. 使用 ${var!"default"} 或 <#if var??> 安全访问 | 开启 DEBUG_HANDLER 可快速定位 |
TemplateNotFoundException | 模板文件路径错误或未打包进 classpath | 1. 检查 setTemplateLoaderPath 2. 确认文件在 src/main/resources 下 | Spring Boot 默认扫描 templates/ |
| 中文乱码 | 模板文件编码与 Configuration 设置不一致 | 1. 模板保存为 UTF-8 2. cfg.setDefaultEncoding("UTF-8") | IDE 和构建工具(Maven)也需设 UTF-8 |
${user.name} 输出 com.example.User@1a2b3c | 对象无 getter 或 FreeMarker 无法读取属性 | 1. 确保有 public String getName() 2. 检查 ObjectWrapper 配置 | Lombok 需启用 @Getter |
宏调用时报 Unknown directive | 未导入宏库或命名空间错误 | 1. 确认 <#import "lib.ftl" as lib> 2. 调用时用 <@lib.macroName> | 宏定义必须在 import 的模板中 |
循环中 _index 未定义 | 循环变量前缀错误 | 应使用 item_index(其中 item 是 #list 中的变量名) | 如 <#list users as u>${u_index}</#list> |
| 自动转义未生效 | 未设置 outputFormat 或使用了 ?no_esc | 1. cfg.setOutputFormat(HTMLOutputFormat.INSTANCE) 2. 避免滥用 ?no_esc | 插值在 HTML 上下文中才自动转义 |
| 模板修改后未生效 | 模板缓存未刷新 | 开发时设置 cfg.setTemplateUpdateDelaySeconds(0) | 生产环境应关闭热更新 |
第九章:代码生成
9.1 配置依赖
| 方法/配置项 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| Maven 引入 FreeMarker | <dependency><groupId>org.freemarker</groupId><artifactId>freemarker</artifactId><version>2.3.32</version></dependency> | 在 Java 项目中引入 FreeMarker 核心库 | 见下方代码块 | 推荐使用最新稳定版;若用于 Spring Boot 项目,可配合 spring-boot-starter-freemarker |
| Gradle 引入 FreeMarker | implementation 'org.freemarker:freemarker:2.3.32' | 在 Gradle 项目中引入依赖 | implementation 'org.freemarker:freemarker:2.3.32' | 注意与 JDK 版本兼容性(FreeMarker 2.3.x 支持 JDK 8+) |
| 初始化 Configuration 对象 | Configuration cfg = new Configuration(Configuration.VERSION_2_3_32); | 创建 FreeMarker 配置实例 | 见下方代码块 | 必须指定模板加载路径和编码,避免中文乱码 |
Maven 依赖:
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
初始化 Configuration:
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setDirectoryForTemplateLoading(new File("templates"));
cfg.setDefaultEncoding("UTF-8");
9.2 生成后端代码
| 方法/操作 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 加载模板 | Template template = cfg.getTemplate("entity.ftl"); | 从模板目录加载 .ftl 文件 | Template entityTpl = cfg.getTemplate("Entity.java.ftl"); | 模板文件名需与实际一致,区分大小写 |
| 准备数据模型 | Map<String, Object> dataModel = new HashMap<>(); | 构建模板渲染所需的数据上下文 | 见下方代码块 | 数据结构需与模板中使用的变量匹配 |
| 渲染输出到文件 | try (Writer fileWriter = new FileWriter(outputFile)) { template.process(dataModel, fileWriter); } | 将渲染结果写入目标 Java 文件 | 见下方代码块 | 确保输出目录存在;注意异常处理(IOException) |
| 常用后端模板类型 | — | 包括 Entity、Mapper、Service、Controller 等 | 模板命名如:Entity.java.ftl、Controller.java.ftl | 每类模板应独立设计,支持字段循环、包名动态替换等 |
准备数据模型示例:
Map<String, Object> model = new HashMap<>();
model.put("className", "User");
model.put("fields", Arrays.asList(
new Field("id", "Long"),
new Field("name", "String")
));
渲染输出到文件示例:
File outputFile = new File("src/main/java/com/example/User.java");
outputFile.getParentFile().mkdirs();
try (Writer writer = new FileWriter(outputFile)) {
entityTpl.process(model, writer);
}
9.3 生成前端代码
| 方法/操作 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 前端模板设计 | 使用 .vue.ftl 或 .jsx.ftl | 生成 Vue/React 组件 | 见下方代码块 | 模板中需转义特殊字符(如 $、{}),或使用 <#noparse> |
| 渲染 Vue 组件 | 同后端流程,但输出路径为 frontend/src/views/ | 生成具体页面组件 | Template vueTpl = cfg.getTemplate("View.vue.ftl"); File vueFile = new File("frontend/src/views/UserView.vue"); | 路径需与前端工程结构对齐;注意文件扩展名 |
| 支持 TS/JSX | 模板文件命名为 .ts.ftl 或 .tsx.ftl | 适配 TypeScript 或 React 项目 | 模板内容包含 interface 或 JSX 语法 | 确保 FreeMarker 不解析 < 为 HTML 实体,可用 <#escape x as x?html> 控制 |
前端模板示例(Vue 组件):
<#-- Vue Component Template -->
<template>
<div class="${className?uncap_first}">${className}</div>
</template>
<script>
export default { name: '${className}' }
</script>
渲染 Vue 组件示例:
Template vueTpl = cfg.getTemplate("View.vue.ftl");
File vueFile = new File("frontend/src/views/UserView.vue");
vueFile.getParentFile().mkdirs();
vueTpl.process(model, new FileWriter(vueFile));
9.4 生成 SQL 脚本
| 方法/操作 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| SQL 模板编写 | 使用 .sql.ftl | 生成建表语句、初始化数据等 | 见下方代码块 | 字段需包含 columnName、sqlType、isPrimaryKey 等属性 |
| 渲染 SQL 文件 | 输出至 db/migration/ 或 resources/sql/ | 用于数据库初始化或 Flyway/Liquibase | Template sqlTpl = cfg.getTemplate("create_table.sql.ftl"); File sqlFile = new File("src/main/resources/sql/user.sql"); | 注意 SQL 关键字大小写、分隔符(如 MySQL 用反引号) |
| 支持多方言 | 在数据模型中传入 dialect(如 "mysql"、"postgresql") | 动态生成不同数据库语法 | 见下方代码块 | 需在模板中做条件判断,避免硬编码 |
SQL 模板示例:
CREATE TABLE ${tableName} (
<#list fields as field>
${field.columnName} ${field.sqlType}<#if field.isPrimaryKey> PRIMARY KEY</#if><#if field_has_next>,</#if>
</#list>
);
渲染 SQL 文件示例:
Template sqlTpl = cfg.getTemplate("create_table.sql.ftl");
File sqlFile = new File("src/main/resources/sql/user.sql");
sqlTpl.process(model, new FileWriter(sqlFile));
多方言支持示例:
<#if dialect == "mysql">
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
</#if>
9.5 生成配置文件
| 方法/操作 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| application.yml 模板 | .yml.ftl 或 .properties.ftl | 生成 Spring Boot 配置 | 见下方代码块 | 使用默认值操作符 ! 避免空指针 |
| 渲染配置文件 | 输出到 src/main/resources/ | 项目启动所需配置 | Template ymlTpl = cfg.getTemplate("application.yml.ftl"); File ymlFile = new File("src/main/resources/application.yml"); | 注意 YAML 缩进必须严格(用空格,非 Tab) |
| 多环境支持 | 通过 profile 参数生成 dev/test/prod 配置 | 适配不同部署环境 | 见下方代码块 | 可结合 Maven profiles 或外部参数传入 |
YAML 模板示例:
server:
port: ${serverPort!8080}
spring:
datasource:
url: jdbc:mysql://localhost:3306/${dbName}
username: ${dbUser!"root"}
password: ${dbPassword!""}
渲染配置文件示例:
Template ymlTpl = cfg.getTemplate("application.yml.ftl");
File ymlFile = new File("src/main/resources/application.yml");
ymlTpl.process(configModel, new FileWriter(ymlFile));
多环境支持示例:
<#if profile == "prod">
logging.level.root=INFO
</#if>
9.6 生成 API 文档
| 方法/操作 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| OpenAPI/Swagger 模板 | .md.ftl 或 .json.ftl | 生成接口文档初稿 | 见下方代码块 | 数据模型需包含 endpoints 列表,每项含 method/path/desc 等 |
| 渲染 Markdown 文档 | 输出为 docs/api.md | 供团队查阅或导入 Swagger | Template docTpl = cfg.getTemplate("api.md.ftl"); File docFile = new File("docs/api.md"); | 可后续用 Swagger UI 或 Redoc 渲染 |
| 与注解联动(可选) | 解析 Controller 中的 @ApiOperation 等 | 自动生成文档模型(需额外解析) | 通常需结合反射或 AST 工具(如 JavaParser) | FreeMarker 本身不解析 Java 代码,需前置步骤提取元数据 |
Markdown 文档模板示例:
# ${apiTitle}
<#list endpoints as endpoint>
## ${endpoint.method} ${endpoint.path}
- Description: ${endpoint.desc}
- Request: ${endpoint.requestType}
- Response: ${endpoint.responseType}
</#list>
渲染 Markdown 文档示例:
Template docTpl = cfg.getTemplate("api.md.ftl");
File docFile = new File("docs/api.md");
docFile.getParentFile().mkdirs();
docTpl.process(apiModel, new FileWriter(docFile));