Article

模板引擎 Freemaker

更新于:2026-07-15

第一章: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\"不支持多行字符串
数字字面量423.14-1.2e5支持整数、浮点、科学计数法所有数字在 FreeMarker 中为 BigDecimal 或 Double
布尔字面量truefalse逻辑值区分大小写
变量引用useruser.nameusers[0]访问数据模型中的变量若中间节点为 null,会抛异常(除非用 !
序列(List)["a", "b", "c"]创建内联序列元素类型可混合
哈希(Map){"name": "Alice", "age": 30}创建内联哈希键必须是字符串
算术运算a + bx * y - z / 2支持 +-*/%除法结果为精确小数(非整数除)
比较运算x == ya < bname != "admin"支持 ==!=<><=>=字符串比较区分大小写
逻辑运算cond1 && cond2!isEmpty支持 &&||!短路求值
内建函数调用name?upper_caselist?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)

内建函数名称语法示例用途代码示例注意事项
stringnum?string("0.##")格式化数字或日期为字符串${price?string("#,##0.00")}依赖 locale
upper_case / lower_casename?upper_case转换字符串大小写${"Hello"?lower_case} → hello仅适用于字符串
cap_firstword?cap_first首字母大写"hello"?cap_first → “Hello”仅处理第一个字符
htmltext?htmlHTML 转义(<&lt; 等)${userInput?html}默认插值已自动转义(若配置了 auto_esc)
urlparam?urlURL 编码${query?url}用于拼接 URL 参数
sizelist?sizestr?size获取序列、哈希或字符串长度<#if users?size > 0>...哈希返回键数量
keys / valuesmap?keysmap?values获取哈希的键或值列表<#list userMap?keys as k>${k}=${userMap[k]}</#list>顺序不确定
sort / sort_byusers?sort_by("name")对序列排序<#list products?sort_by("price") as p>...支持嵌套属性(如 "address.city"
splittext?split(",")按分隔符拆分字符串为序列<#assign parts = "a,b,c"?split(",")>返回序列
replacetext?replace("old", "new")字符串替换"abc"?replace("b", "X") → “aXc”支持正则
existsvariable?exists判断变量是否存在(不推荐,建议用 ??<#if user.name?exists>...已废弃,优先使用 ??
has_contentstr?has_content判断字符串/序列/哈希是否非空<#if list?has_content>...?size > 0 更安全(防 null)

3.3 哈希表、序列与集合操作

操作类型语法/方法用途代码示例注意事项
访问哈希值map.keymap["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 替代方案使用多个 #elseifFreeMarker 无原生 #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 用 macroinclude 会增加文件 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(永久缓存)
自动转义 HTMLsetAutoEscapingPolicy(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渲染时抛出 TemplateExceptioncfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);开发阶段快速定位错误默认行为(某些版本)
DEBUG_HANDLER打印错误信息到输出流,并继续渲染cfg.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER);调试时查看缺失变量等警告输出含错误标记(如 [ERROR: ...]
IGNORE_HANDLER忽略异常,输出为空cfg.setTemplateExceptionHandler(TemplateExceptionHandler.IGNORE_HANDLER);容忍部分模板错误(不推荐)可能掩盖严重问题
自定义异常处理器实现 TemplateExceptionHandler 接口cfg.setTemplateExceptionHandler(myHandler);记录日志、返回友好提示等handleTemplateException 方法中可控制输出
常见异常类型TemplateNotFoundExceptionInvalidReferenceExceptionNonHashException文件不存在、访问 null 对象属性、对非哈希使用 .key可通过异常类型做精细化处理
模板语法错误getTemplate() 时抛出 ParseException需在加载模板时 try-catch模板语法错误(如未闭合标签)与运行时异常(TemplateException)不同

6.4 多语言与国际化支持

功能名称实现方式用途代码示例注意事项
使用 ResourceBundleJava 层加载 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.ftltemplates/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 Beandata.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.Datejava.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 StarterMaven 依赖见下方代码块自动配置 FreeMarkerConfigurer
模板路径默认 classpath:/templates/spring.freemarker.template-loader-path: classpath:/views/模板文件放 src/main/resources/views/路径必须以 / 开头
后缀设置默认 .ftlspring.freemarker.suffix: .html模板文件可命名为 home.html
编码设置默认 UTF-8spring.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}减少内建函数重复调用
控制输出缓冲使用带缓冲的 Writernew 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模板文件路径错误或未打包进 classpath1. 检查 setTemplateLoaderPath 2. 确认文件在 src/main/resourcesSpring 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_esc1. 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 引入 FreeMarkerimplementation '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.ftlController.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/LiquibaseTemplate 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供团队查阅或导入 SwaggerTemplate 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));