1. 简介
FreeMarker 是由 Apache 基金会维护的一款 Java 模板引擎。它使用一种称为 FTL(FreeMarker Template Language)的语言,能够生成各种基于文本的输出格式,比如 HTML 页面、邮件内容或 XML 文件。
本文将带你掌握 FreeMarker 的常用功能。虽然它的功能非常强大且高度可配置(例如支持与 Spring 框架无缝集成),但我们聚焦于日常开发中最实用的核心操作,避免陷入配置细节的泥潭。
准备好了吗?我们开始吧!
2. 快速概览
要在页面中注入动态内容,必须使用 FreeMarker 能识别的语法:
- ✅ **插值表达式
${...}
**:大括号内的表达式会被实际值替换。例如${1 + 2}
输出3
,${userName}
输出变量值。这叫“插值”(interpolation)。 - ✅ FTL 标签:类似 HTML 标签,但以
#
或@
开头,如<#if condition></#if>
,FreeMarker 会解析并执行它们。 - ✅ 注解(Comment):以
<#--
开始,-->
结束,不会出现在最终输出中。
3. include 标签:复用模板片段
<#include>
标签是践行 DRY(Don't Repeat Yourself)原则的利器。我们可以把重复的 HTML 片段(如页头、菜单)抽成独立文件,在多个模板中复用。
比如,我们把菜单抽成 menu.ftl
文件:
<a href="#dashboard">Dashboard</a>
<a href="#newEndpoint">Add new endpoint</a>
然后在主页面中直接引入:
<!DOCTYPE html>
<html>
<body>
<#include 'fragments/menu.ftl'>
<h6>Dashboard page</h6>
</body>
</html>
✅ 好处:修改菜单只需改一处,所有页面自动更新。
✅ 注意:被 include 的片段本身也可以包含其他 FTL 标签,非常灵活。
4. 处理值的存在性(null 安全)
FTL 把 null
视为“缺失值”,直接访问会抛异常。所以,null 安全是模板开发的必修课。
4.1 判断值是否存在:??
操作符
${student??} <!-- 返回 true 或 false -->
4.2 提供默认值:!
操作符
当值缺失时,使用默认值兜底:
${student!'John Doe'}
4.3 处理嵌套属性
对于 student.address.street
这种深层嵌套,必须用括号包裹,否则语法错误:
${(student.address.street)??}
4.4 实战示例
<p>Testing is student property exists: ${student???c}</p>
<p>Using default value for missing student: ${student!'John Doe'}</p>
<p>Wrapping student nested properties: ${(student.address.street)???c}</p>
如果 student
为 null
,输出:
<p>Testing is student property exists: false</p>
<p>Using default value for missing student: John Doe</p>
<p>Wrapping student nested properties: false</p>
⚠️ 关键点:??
返回布尔值,要显示在页面需用 ?c
转成字符串(true
/false
),否则会报错。
5. if-else 控制结构
FreeMarker 支持标准的条件判断:
<#if condition>
<!-- 条件为真时执行 -->
<#elseif condition2>
<!-- condition2 为真时执行 -->
<#elseif condition3>
<!-- condition3 为真时执行 -->
<#else>
<!-- 所有条件都为假时执行 -->
</#if>
常用比较操作符
操作符 | 含义 | 替代写法 |
---|---|---|
== |
等于 | |
!= |
不等于 | |
lt |
小于 | < |
gt |
大于 | > |
lte |
小于等于 | <= |
gte |
大于等于 | >= |
?? |
值是否存在 | |
sequence?seqContains(x) |
序列是否包含 x |
⚠️ 大坑警告:>
和 >=
会被 FTL 解析器误认为是标签结束符!
✅ 解决方案:优先使用 gt
/gte
,或用括号包裹,如 (x > y)
。
示例
<#if status??>
<p>${status.reason}</p>
<#else>
<p>Missing status!</p>
</#if>
输出:
<!-- status 存在时 -->
<p>404 Not Found</p>
<!-- status 不存在时 -->
<p>Missing status!</p>
6. 容器与子变量
FreeMarker 中有三种主要的容器类型:
- ✅ Hash:键值对集合,键唯一,无序。类似 Java 的
Map
。 - ✅ Sequence:有序列表,可通过索引访问。类似 Java 的
List
。 - ✅ Collection:特殊的 Sequence,只能迭代,不能通过索引访问或获取大小。
6.1 迭代容器
基本迭代
遍历 Sequence:
<#list sequence as item>
<p>${item}</p>
</#list>
遍历 Hash(获取 key 和 value):
<#list hash as key, value>
<p>${key}: ${value}</p>
</#list>
增强迭代(带空值处理)
<#list statuses>
<ul>
<#items as status>
<li>${status}</li>
</#items>
</ul>
<#else>
<p>No statuses available</p>
</#list>
当 statuses
为 ["200 OK", "404 Not Found", "500 Internal Server Error"]
时,输出:
<ul>
<li>200 OK</li>
<li>404 Not Found</li>
<li>500 Internal Server Error</li>
</ul>
6.2 常用操作
- Hash:
.keys
(获取所有键),.values
(获取所有值)。 - Sequence:
chunk
,join
:分块或合并。reverse
,sort
,sortBy
:排序。first
,last
:获取首尾元素。size
:获取大小。seqContains
,seqIndexOf
:查找元素。
7. 类型处理(Built-ins)
FreeMarker 提供了丰富的内置函数(built-ins)来处理不同类型的数据。
7.1 字符串处理
<p>${'http://myurl.com/?search=Hello World'?urlPath}</p>
<p>${'Using " in text'?jsString}</p>
<p>${'my value'?upperCase}</p>
<p>${'2019-01-12'?date('yyyy-MM-dd')}</p>
输出:
<p>http%3A//myurl.com/%3Fsearch%3DHello%20World</p>
<p>MY VALUE</p>
<p>Using \" in text</p>
<p>12.01.2019</p>
✅ 说明:
urlPath
会转义特殊字符,但保留/
。jsString
对 JS 字符串进行转义。date
需要指定解析格式,否则可能因本地化设置出错。
7.2 数字处理
<p>${(7.3?round + 3.4?ceiling + 0.1234)?string('0.##')}</p>
<!-- (7 + 4 + 0.1234) 保留两位小数 -->
输出:<p>11.12</p>
round
:四舍五入。floor
:向下取整。ceiling
:向上取整。string
:可指定格式,如'0.##'
。
7.3 日期处理
<p>${.now?time?string('HH:mm')}</p>
输出(示例):
<p>15:39</p>
.now
:当前时间。?time
:提取时间部分。?string('HH:mm')
:格式化输出。
8. 异常处理
模板执行出错时,有两种处理方式。
8.1 attempt-recover 机制
<#attempt>
<p>Attribute is ${attributeWithPossibleValue??}</p>
<#recover>
<p>Attribute is missing</p>
</#attempt>
✅ 特点:
attempt
和recover
必须成对出现。- 出错时,
attempt
块的输出会被回滚,只执行recover
块。
输出(当 attributeWithPossibleValue
缺失时):
<p>Preparing to evaluate</p>
<p>Attribute is missing</p>
<p>Done with the evaluation</p>
8.2 全局异常处理器(Spring Boot)
在 application.properties
中配置:
# 直接抛出异常(生产环境慎用)
spring.freemarker.setting.template_exception_handler=rethrow
# 输出调试信息(含堆栈)
spring.freemarker.setting.template_exception_handler=debug
# 输出格式化堆栈(适合浏览器查看)
spring.freemarker.setting.template_exception_handler=html_debug
# 忽略错误,继续执行(最宽容)
spring.freemarker.setting.template_exception_handler=ignore
# 使用默认处理器
spring.freemarker.setting.template_exception_handler=default
✅ 建议:开发环境用 html_debug
,生产环境用 ignore
避免页面崩溃。
9. 调用 Java 方法
有时需要在模板中调用 Java 逻辑。
9.1 访问静态成员
在 Java 代码中暴露静态类:
model.addAttribute("statics", new DefaultObjectWrapperBuilder(new Version("2.3.28"))
.build().getStaticModels());
在模板中使用:
<#assign MathUtils=statics['java.lang.Math']>
<p>PI value: ${MathUtils.PI}</p>
<p>2^10 is: ${MathUtils.pow(2, 10)}</p>
输出:
<p>PI value: 3.142</p>
<p>2^10 is: 1,024</p>
9.2 访问 Bean 方法
最简单!直接用 .
调用。
Java 代码:
model.addAttribute("random", new Random());
模板:
<p>Random value: ${random.nextInt()}</p>
输出(示例):
<p>Random value: 1,329,970,768</p>
9.3 自定义方法
实现 TemplateMethodModelEx
接口:
public class LastCharMethod implements TemplateMethodModelEx {
public Object exec(List arguments) throws TemplateModelException {
if (arguments.size() != 1 || StringUtils.isEmpty(arguments.get(0)))
throw new TemplateModelException("Wrong arguments!");
String argument = arguments.get(0).toString();
return argument.charAt(argument.length() - 1);
}
}
注册到模型:
model.addAttribute("lastChar", new LastCharMethod());
在模板中调用:
<p>Last char example: ${lastChar('mystring')}</p>
输出:
<p>Last char example: g</p>
10. 总结
本文覆盖了 FreeMarker 开发中的核心操作:从基础语法、null 安全、流程控制,到数据处理、异常管理和 Java 方法调用。掌握这些,足以应对绝大多数模板开发场景。
所有示例代码均已上传至 GitHub: https://github.com/tech-tutorials/spring-freemarker-demo