FreeMarker 语法概要:模板核心语法与常用指令速览

本文是 FreeMarker 2.3.x 官方文档整理,涵盖数据模型、取值语法、指令(if/list/assign/macro)、内置函数、Spring Boot 集成以及 15 个常见坑总结,帮助快速掌握 FreeMarker 用法。

本文参考 FreeMarker 官方文档(2.3.x)整理。

FreeMarker 是 Java 服务端模板引擎,与 Thymeleaf 是“同赛道双雄”:Thymeleaf 偏 HTML 自然模板,FreeMarker 用 ${...} 和 <#...>指令语法,生成能力更通用(HTML、XML、SQL、代码、邮件都行)。

核心概念是 数据模型(Data Model)+ 模板(Template)+ 指令(Directive)+ 内置函数(Built-ins)

这篇笔记覆盖日常开发 90% 的 FreeMarker 用法,包含数据模型、取值语法、指令(if/list/assign/macro)、内置函数、格式化、Spring Boot 集成等知识点。

一、基础认知

1.1 三者关系

模板(Template) + 数据模型(Data Model) → 输出(HTML/XML/邮件/代码)
  • 模板:静态文本 + FreeMarker 指令(${}插值、<#>标签)

  • 数据模型:一棵树(root 对象 + 子变量),由 Java 通过 model.addAttribute() 传入

  • 输出:纯文本,不只是 HTML —— 生成 SQL、Java 代码、配置文件都行

1.2 快速上手

maven 依赖如下:

<!-- Spring Boot 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

注意,Freemarker 默认模板位置为 src/main/resources/templates/*.ftl,模板的后缀可以为 .ftl 或 .ftlh。

编写控制器,渲染 templates/user.ftl 模板:

@Controller
public class UserController {

    @GetMapping("/user")
    public String user(Model model) {
        // 添加数据,HTML页面可以使用 ${} 语法进行读取
        model.addAttribute("name", "张三");
        model.addAttribute("user", new User("李四", 25, true));
        model.addAttribute("users", List.of(...));
        return "user"; // 渲染 templates/user.ftl
    }

}

HTML模板代码:

<!-- templates/user.ftl -->
<html>
<body>
  <h1>你好,${name}!</h1>
</body>
</html>

FreeMarker vs Thymeleaf 一句话

  • Thymeleaf 模板是“合法 HTML + 属性”。

  • FreeMarker 模板是“文本 + ${}/<#>指令”,FreeMarker 更灵活,但不直接打开看原型效果。

二、取值语法(数据模型访问)

2.1 基本取值

${name}                  <!-- 标量 -->
${user.name}             <!-- 对象属性(自动调用 getter) -->
${user.address.city}     <!-- 嵌套 -->
${users[0]}              <!-- 列表按下标 -->
${users[0].name}         <!-- 组合 -->

<!-- 列表/集合遍历的两种取法 -->
${users[1]}
${users?first}  ${users?last}  ${users?size}

<!-- 字典/Map -->
${map["key"]}            <!-- 或 ${map.key}(key 为合法标识符时) -->

<!-- 算术/逻辑 -->
${age + 1}  ${age * 2}
${(a + b) * 3}

2.2 空值处理(重点)

<!-- 默认值:! 后给兜底 -->
${user.name!}             <!-- null 或不存在 → 空字符串 -->
${user.name!"游客"}       <!-- null → "游客" -->

<!-- 链式空安全 -->
${user.address.city!}     <!-- user 或 address 为 null 都不报错 -->

<!-- 判断是否存在 -->
<#if user??>有用户</#if>
<#if user.name??>有姓名</#if>

FreeMarker 最大坑:变量不存在或为 null 时直接 ${x} 会抛异常,任何可能为空的取值都加 ! 是铁律。

2.3 字符串拼接

${"你好," + name + "!"}
${"你好,${name}!"}        <!-- 插值(有引号场景) -->
${'你好,${name}'}          <!-- 单引号同样支持 -->

三、指令

3.1<#if>条件

<#if user.admin>
  管理员
<#elseif user.vip>
  VIP 用户
<#else>
  普通用户
</#if>

<!-- 逻辑:==  !=  <  >  <=  >=   &&  ||  ! -->
<#if age >= 18 && user.active>成年且启用</#if>
<#if status == "done">完成</#if>          <!-- 字符串比较 -->

<!-- 判断空集合 -->
<#if users?has_content>有数据</#if>

3.2 <#list>循环(重点)

<!-- 基本循环 -->
<#list users as u>
  <p>${u.name} - ${u.age}</p>
</#list>

<!-- 循环状态:u_index(0开始)、u_has_next、item_cycle -->
<#list users as u>
  <p>${u_index + 1}. ${u.name}</p>          <!-- 序号从1 -->
</#list>

<!-- 隔行变色(item_cycle 轮换值) -->
<#list users as u>
  <tr class="${u?item_cycle('odd','even')}">...</tr>
</#list>

<!-- 空列表提示 -->
<#list users as u>
  ...
<#else>
  <p>暂无数据</p>
</#list>

<!-- break 跳出 / 指定范围循环 -->
<#list 1..5 as i>${i}</#list>              <!-- 1 2 3 4 5 -->

⚠️注意:循环状态 ?index(0 基)、?counter(1 基)、?item_cycle(v1,v2) 轮换是三个高频内置函数。

3.3 <#assign>定义变量

<#assign total = 0>
<#assign name = "张三">
<#assign greeting = "你好,${name}">
<#assign x = 10 y = 20>        <!-- 一次定义多个 -->

<#assign total = total + 1>    <!-- 更新 -->

3.4 <#macro>宏(复用模板片段,重点)

<!-- 定义宏 -->
<#macro greeting name>
  <p>你好,${name}!</p>
</#macro>

<!-- 调用 -->
<@greeting name="张三"/>
<@greeting "李四"/>             <!-- 简写(按位置传参) -->


<!-- 宏默认值 -->
<#macro badge status="normal">
  <span class="badge-${status}">${status}</span>
</#macro>
<@badge/>                       <!-- 用默认 normal -->
<@badge status="vip"/>

<!-- 宏体内内容(nested) -->
<#macro card title>
  <div class="card">
    <h3>${title}</h3>
    <#nested/>                  <!-- 放调用者提供的内部内容 -->
  </div>
</#macro>
<@card title="用户卡片">这里是内容</@card>

3.5 <#include>与 <#import>(复用,重点)

<!-- include:把别的模板插进来(直接包含,多次包含会重复) -->
<#include "/common/header.ftl">

<!-- import:导入宏库(可避免宏名冲突,用命名空间调用) -->
<#import "/common/macros.ftl" as m>
<@m.greeting name="张三"/>

四、内置函数

内置函数语法:${变量?函数},是 FreeMarker 的“方法库”。

4.1 字符串

${s?upper_case}        <!-- 大写 -->
${s?lower_case}        <!-- 小写 -->
${s?length}            <!-- 长度 -->
${s?trim}              <!-- 去空格 -->
${s?substring(0, 3)}   <!-- 截取 -->
${s?replace("a","b")}  <!-- 替换 -->
${s?contains("x")?string("含","不含")}   <!-- 判断 -->
${s?starts_with("ab")} <!-- 前缀判断 -->

4.2 数字与格式化

${n?c}                 <!-- 原样输出数字(去掉逗号分组,重要!) -->
${n?string("0.00")}    <!-- 2位小数:3.14 -->
${n?string("#.##")}    <!-- 最多2位 -->
${n?string("000")}     <!-- 补零:007 -->
${n?round}  ${n?floor}  ${n?ceiling}
${n?string.percent}    <!-- 百分比 -->

<!-- 金额千分位 -->
${price?string("0,000.00")}   <!-- 12,345.67 -->

⚠️注意:?c 是高频必用:FreeMarker 默认按 Locale 对数字加千分位(1234567 → 1,234,567),在 HTML value 或 JS 里会出错,用 ${n?c} 原样输出。

4.3 日期时间

<!-- 日期转字符串 -->
${date?string("yyyy-MM-dd")}
${time?string("HH:mm:ss")}
${datetime?string("yyyy-MM-dd HH:mm:ss")}

<!-- 常用 -->
${date?date}            <!-- 只显示日期 -->
${date?datetime}        <!-- 显示日期时间 -->
${.now}                 <!-- 当前时间 -->
${.now?string("yyyy-MM-dd")}

4.4 集合与判断

${list?size}            <!-- 长度 -->
${list?first}  ${list?last}
${list?seq_contains(x)} <!-- 是否包含 -->
${list?seq_index_of(x)} <!-- 下标 -->
${list?reverse}         <!-- 反转 -->
${list?sort}            <!-- 排序 -->
${list?sort_by("age")}  <!-- 按属性排序 -->
${list?join(", ")}      <!-- 用分隔符连接成字符串 -->

${x?has_content}        <!-- 变量存在且非空(用于 if) -->
${x?is_string}  ${x?is_number}  ${x?is_boolean}  ${x?is_sequence}

4.5 输出控制

${html?html}            <!-- HTML 转义(防 XSS) -->
${json?js_string}       <!-- JS 字符串转义 -->
${url?url}              <!-- URL 转义 -->

⚠️注意:?html防 XSS:输出用户提交的内容务必转义,${content?html}。

五、序列与哈希

<!-- 范围序列 -->
<#list 1..10 as i>${i}</#list>
<#list 1..10 step 2 as i>${i}</#list>    <!-- 1 3 5 7 9 -->

<!-- 内联序列 -->
<#list ["a","b","c"] as s>${s}</#list>

<!-- 内联哈希(Map 字面量) -->
<#assign m = {"name": "张三", "age": 25}>
${m["name"]}
${m.name}

<!-- 遍历 Map -->
<#list m?keys as k>${k}=${m[k]}</#list>

六、特殊变量与全局配置

${.now}                    <!-- 当前时间 -->
${.lang}                   <!-- 语言 -->
${.version}                <!-- FreeMarker 版本 -->

七、模板安全

FreeMarker 模板能执行任意 Java 方法调用(${user.getClass()...}),模板即代码。要点:

  1. 模板文件只放受信来源,不要用用户输入作为模板名或内容

  2. 不把 Configuration 或任意 Bean 暴露给模板(api_builtin_enabled=false)

  3. 用 spring.freemarker.settings.api_builtin_enabled=false 关闭 ?api 内部访问

  4. 输出用户内容一律 ?html 转义

八、Spring Boot 集成要点

配置信息如下:

# application.properties
spring.freemarker.cache=false              # 开发关缓存
spring.freemarker.suffix=.ftl              # 后缀
spring.freemarker.content-type=text/html;charset=UTF-8    # 编码
spring.freemarker.settings.number_format=0.##       # 全局数字格式(防千分位坑)
spring.freemarker.settings.date_format=yyyy-MM-dd   # 日期格式
spring.freemarker.settings.time_format=HH:mm:ss     # 时间格式
spring.freemarker.settings.datetime_format=yyyy-MM-dd HH:mm:ss  # 日期时间格式
// Controller 与 Thymeleaf 一样返回视图名
model.addAttribute("list", ...);
return "user";

// 全局变量:@ControllerAdvice + @ModelAttribute

全局配 number_format=0.## 能避免大量 ${n?c} 补丁,是团队级最佳实践。

九、常见坑总结

  1. 变量不存在/null 直接取:${user.name} 但 user 为 null → 异常。任何可能为空加 !(${user.name!})。

  2. 数字千分位坑:${1234567} 默认渲染成 1,234,567,用在 value 属性/JS 里出错 —— 用 ${n?c} 或全局 number_format。

  3. <#if>里字符串比较:<#if status == done>少了引号会把 done当变量(不存在报错),必须 == "done"。

  4. 布尔判断:<#if user.admin> 是对的;<#if user.admin == true> 也行但多余;<#if user.admin == "true"> 类型错(布尔 vs 字符串)。

  5. <#list>空列表:没数据直接渲染空白,用 <#else>分支给提示。

  6. <#include>vs <#import>:include 重复包含同一文件会重复输出(变量/宏重定义报错);复用宏用 import。

  7. 宏参数没传:宏内部用了未传参数 → 未定义错误;给宏参数加默认值(<#macro m x="1">)兜底。

  8. 日期类型不匹配:java.sql.Date/LocalDate/Timestamp 混用,?string 格式化报错 —— 统一类型,LocalDateTime 需 ?string("yyyy-MM-dd HH:mm:ss")。

  9. ?c 用在字符串上报错:?c 只能用于数字,字符串用 ! 或 ?string。

  10. 模板路径大小写/斜杠:<#include "/common/Header.ftl"> 路径对大小写敏感(Linux),注意全小写约定。

  11. 循环里修改集合:<#list> 遍历时不能改集合(无 remove),用 assign 重建或过滤。

  12. ${} 里用了 <#> 指令语法:插值里不能写指令,${<#if>...} 非法 —— 逻辑放外面。

  13. 数字直接拼接字符串出错:${"编号" + 123}可以(自动转字符串),但 ${123 + "abc"} 报错(数字+非数字)。

  14. HTML 注入:用户输入不转义输出即 XSS,${content?html}。

  15. 缓存忘了关:开发期 spring.freemarker.cache=false,否则改模板要重启。

十、附录

# 取值
${var} ${obj.prop} ${list[0]} ${map.key}
${x!} ${x!"默认"} ${x??}              # 空值处理

# 指令
<#if 条件>...<#elseif>...<#else>...</#if>
<#list list as item>${item_index}.${item}<#else>空</#list>
<#assign x = 值> <#include "file.ftl"> <#import "m.ftl" as m>

# 宏
<#macro name param="默认"><#nested/></#macro>
<@name param="值">内容</@name>

# 内置函数
?upper_case ?lower_case ?length ?trim ?substring(0,3)
?c(数字原样) ?string("0.00") ?string("yyyy-MM-dd")
?size ?first ?last ?join(", ") ?seq_contains(x) ?item_cycle(a,b)
?has_content ?html(XSS转义) ?js_string

# Spring Boot 配置
spring.freemarker.cache=false
spring.freemarker.settings.number_format=0.##

FreeMarker 上手路径:先搞懂数据模型 + ${}取值 + !空值兜底,再练三大指令(if/list/assign),然后学内置函数(重点 ?c、?string、?html),最后是宏和 import 复用。

官方文档 https://freemarker.apache.org/docs/ 的“Template Author's Guide”最权威,如果对上面存在疑虑,可以参考官方文档。

记住:FreeMarker 是“文本 + 指令”的通用模板引擎 —— 空值必加 !、数字记得 ?c、输出用户内容要 ?html,这三条能避开 80% 的坑。

 

一个不注意小事情的人,永远不会成功大事业。——戴尔·卡耐基
0 不喜欢
说说我的看法 -
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号