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>

如果 studentnull,输出:

<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>

特点

  • attemptrecover 必须成对出现。
  • 出错时,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


原始标题:FreeMarker Common Operations | Baeldung