FreeMarker 语法全面详解
FreeMarker 是一种功能强大的模板引擎,其语法设计既简洁又灵活。下面我将从基础到高级全面解析 FreeMarker 的语法体系,包含大量示例和实用技巧。
一、基础语法结构
1.1 基本模板结构
FreeMarker 模板是普通的文本文件,其中可以包含:
- 静态文本:直接输出
- FTL 标签:<#...> 或 [@...] 形式
- 插值:${...} 形式
<html>
<head>
<title>Welcome!</title>
</head>
<body>
<#-- 这是一个FTL注释 -->
<h1>Welcome ${user}!</h1>
<p>Current date: ${.now?string('yyyy-MM-dd')}</p>
</body>
</html>
1.2 注释语法
类型 | 语法 | 说明 |
FTL注释 | <#-- 注释内容 --> | 不会出现在输出中 |
HTML注释 | <!-- 注释内容 --> | 会出现在输出中 |
<#-- 这个注释不会出现在最终输出中 -->
<!-- 这个HTML注释会出现在输出中 -->
二、插值(Interpolation)
2.1 基本插值
<p>Hello, ${name}!</p>
<p>Total: ${price * quantity}</p>
<p>Status: ${user.isActive?string("active", "inactive")}</p>
2.2 插值中的操作符
2.2.1 常用操作符
操作符 | 说明 | 示例 |
! | 默认值 | ${name!"anonymous"} |
?? | 存在性检查 | ${name??} |
? | 内建函数调用 | ${name?upper_case} |
. | 访问属性 | ${user.name} |
[] | 访问集合/数组 | ${users[0].name} |
2.2.2 运算符优先级
- ? ! ??
- [] .
- - + (一元)
- * / %
- + - (二元)
- < > <= >=
- == !=
- &&
- ||
2.3 特殊变量
FreeMarker 提供了一些内置的特殊变量:
变量 | 说明 |
.now | 当前日期时间 |
.data_model | 整个数据模型 |
.globals | 全局变量 |
.vars | 当前命名空间的变量 |
.namespace | 当前命名空间 |
.main | 主命名空间 |
.lang | 当前语言 |
.locale | 当前地域 |
.current_template_name | 当前模板名 |
<p>Current time: ${.now?time}</p>
<p>Template: ${.current_template_name}</p>
三、指令(Directives)
3.1 常用指令概览
指令 | 说明 | 示例 |
if/elseif/else | 条件判断 | <#if x == 1>...</#if> |
list | 遍历集合 | <#list items as item>...</#list> |
include | 包含模板 | <#include "header.ftl"> |
assign | 定义变量 | <#assign x = 1> |
macro | 定义宏 | <#macro greet>Hello!</#macro> |
function | 定义函数 | <#function add a b><#return a + b></#function> |
switch/case/default | 多条件分支 | <#switch x><#case 1>...<#default>...</#switch> |
3.2 条件指令(if/elseif/else)
<#if temperature < 0>
<p>It's freezing!</p>
<#elseif temperature < 15>
<p>It's chilly.</p>
<#else>
<p>It's warm.</p>
</#if>
比较运算符:
- == != > < >= <=
- gt lt gte lte (避免与HTML标签冲突)
<#if x gt 10> <#-- 等同于 x > 10 -->
<p>Greater than 10</p>
</#if>
3.3 循环指令(list)
3.3.1 基本循环
<ul>
<#list users as user>
<li>${user.name} (${user.email})</li>
</#list>
</ul>
3.3.2 循环状态变量
<table>
<#list products as product>
<tr class="${product?item_cycle('odd', 'even')}">
<td>${product?index + 1}</td>
<td>${product.name}</td>
<td>${product.price}</td>
</tr>
</#list>
</table>
循环状态变量属性:
属性 | 说明 |
index | 当前索引 (从0开始) |
counter | 当前计数 (从1开始) |
item | 当前项 (同循环变量) |
has_next | 是否有下一项 |
is_first | 是否第一项 |
is_last | 是否最后一项 |
3.3.3 遍历Map
<#list userMap?keys as key>
Key: ${key}, Value: ${userMap[key]}
</#list>
<#-- 更简洁的方式 -->
<#list userMap as key, value>
${key} = ${value}
</#list>
3.4 变量定义(assign)
<#assign name = "Alice">
<#assign age = 25>
<#assign isAdult = (age >= 18)>
<#-- 定义多个变量 -->
<#assign {
"name": "Bob",
"age": 30,
"city": "New York"
}>
<#-- 作用域控制 -->
<#assign x = 1 in myNamespace>
3.5 包含指令(include)
<#-- 包含其他模板 -->
<#include "header.ftl">
<#-- 带参数传递 -->
<#include "user_info.ftl" with {
"user": currentUser,
"showDetails": true
}>
3.6 宏指令(macro)
3.6.1 基本宏定义
<#macro greet name age>
<p>Hello ${name}, you are ${age} years old.</p>
</#macro>
<#-- 使用宏 -->
<@greet name="Alice" age=25/>
3.6.2 嵌套内容
<#macro bordered>
<div style="border: 1px solid black; padding: 10px;">
<#nested>
</div>
</#macro>
<@bordered>
<p>This content will be bordered.</p>
</@bordered>
3.6.3 命名参数与默认值
<#macro alert type="info" message>
<div class="alert alert-${type}">
${message}
</div>
</#macro>
<@alert message="This is important!"/>
<@alert type="danger" message="Error occurred!"/>
3.7 函数指令(function)
<#function avg x y>
<#return (x + y) / 2>
</#function>
<p>Average: ${avg(10, 20)}</p>
四、内建函数(Built-ins)
FreeMarker 提供了丰富的内建函数,用于常见的数据处理。
4.1 字符串处理
函数 | 说明 | 示例 |
?upper_case | 转大写 | ${"hello"?upper_case} → "HELLO" |
?lower_case | 转小写 | ${"HELLO"?lower_case} → "hello" |
?cap_first | 首字母大写 | ${"hello"?cap_first} → "Hello" |
?starts_with | 是否以开头 | ${"hello"?starts_with("he")} → true |
?ends_with | 是否以结尾 | ${"hello"?ends_with("lo")} → true |
?contains | 是否包含 | ${"hello"?contains("ell")} → true |
?substring | 子字符串 | ${"hello"?substring(1,3)} → "el" |
?length | 长度 | ${"hello"?length} → 5 |
?trim | 去空格 | ${" hello "?trim} → "hello" |
?replace | 替换 | ${"hello"?replace("l","x")} → "hexxo" |
4.2 数字处理
函数 | 说明 | 示例 |
?string | 格式化 | ${1234.567?string["0.##"]} → "1234.57" |
?round | 四舍五入 | ${1.5?round} → 2 |
?floor | 向下取整 | ${1.9?floor} → 1 |
?ceiling | 向上取整 | ${1.1?ceiling} → 2 |
?abs | 绝对值 | ${-5?abs} → 5 |
4.3 日期时间处理
函数 | 说明 | 示例 |
?date | 仅日期部分 | ${.now?date} → "2023-07-20" |
?time | 仅时间部分 | ${.now?time} → "15:30:45" |
?datetime | 日期时间 | ${.now?datetime} → "2023-07-20 15:30:45" |
?string(format) | 自定义格式 | ${.now?string("yyyy-MM-dd HH:mm")} |
?date_if_unknown | 强制为日期 | ${someDate?date_if_unknown} |
4.4 集合处理
函数 | 说明 | 示例 |
?size | 集合大小 | ${users?size} |
?first | 第一个元素 | ${users?first.name} |
?last | 最后一个元素 | ${users?last.name} |
?reverse | 反转集合 | <#list users?reverse as user> |
?sort | 排序 | <#list users?sort_by("name") as user> |
?filter | 过滤 | <#list users?filter(u -> u.age > 18) as user> |
?map | 转换 | <#list users?map(u -> u.name) as name> |
?join | 连接为字符串 | ${tags?join(", ")} |
4.5 其他实用内建函数
函数 | 说明 | 示例 |
?has_content | 检查有内容 | ${name?has_content} |
?default | 默认值 | ${name?default("anonymous")} |
?is_* | 类型检查 | ${value?is_string}, ${value?is_number} |
?eval | 执行字符串表达式 | ${"1 + 2"?eval} → 3 |
?interpret | 解析FTL字符串 | <#assign template="Hello ${name}!">${template?interpret} |
五、命名空间与模板组织
5.1 命名空间基础
<#-- 定义命名空间变量 -->
<#assign x = 1 in myNamespace>
<#-- 访问命名空间变量 -->
${myNamespace.x}
5.2 模板导入(import)
<#-- lib/macros.ftl -->
<#macro copyright year>
<p>Copyright (c) ${year} My Company</p>
</#macro>
<#-- 主模板 -->
<#import "/lib/macros.ftl" as my>
<@my.copyright year=2023/>
5.3 全局变量与局部变量
<#-- 全局变量,所有模板可见 -->
<#global appName = "My Application">
<#-- 局部变量,只在当前模板或命名空间可见 -->
<#assign localVar = "temp value">
六、高级特性
6.1 异常处理
<#attempt>
<#-- 可能出错的代码 -->
${undefinedVariable}
<#recover>
<#-- 出错时执行的代码 -->
<p>Error occurred: ${.error}</p>
</#attempt>
6.2 自定义指令
public class UpperDirective implements TemplateDirectiveModel {
@Override
public void execute(Environment env, Map params,
TemplateModel[] loopVars, TemplateDirectiveBody body)
throws TemplateException, IOException {
if (body != null) {
StringWriter writer = new StringWriter();
body.render(writer);
String content = writer.toString().toUpperCase();
env.getOut().write(content);
}
}
}
模板中使用:
<@upper>
This text will be uppercased.
</@upper>
6.3 模板继承
base.ftl:
<!DOCTYPE html>
<html>
<head>
<title><#block "title">Default Title</#block></title>
</head>
<body>
<#block "content">
Default content
</#block>
</body>
</html>
child.ftl:
<#import "base.ftl" as layout>
<@layout>
<#block "title">
Custom Title
</#block>
<#block "content">
<h1>Custom Content</h1>
<p>This replaces the default content.</p>
</#block>
</@layout>
6.4 动态模板处理
<#-- 动态选择模板 -->
<#assign templateName = user.isAdmin?then("admin.ftl", "user.ftl")>
<#include templateName>
<#-- 动态内容生成 -->
<#assign dynamicContent>
<#if condition>
<p>Some content</p>
<#else>
<div>Other content</div>
</#if>
</#assign>
${dynamicContent}
七、实用技巧与最佳实践
7.1 避免空指针
<#-- 安全访问链式属性 -->
${user.address.city!}
<#-- 多级默认值 -->
${user.address.city!"Unknown"}
<#-- 存在性检查 -->
<#if user.address??>
${user.address.city}
</#if>
7.2 复杂条件简化
<#-- 代替多个if-else -->
<#assign statusColor = (status == "active")?then("green", "red")>
<#-- switch-case 替代方案 -->
<#assign message = {
"success": "Operation succeeded",
"error": "An error occurred",
"warning": "Please check your input"
}[status]!defaultMessage>
7.3 性能优化
- 避免在模板中计算:
- <#-- 不推荐 -->
<#list heavyOperation() as item>
<#-- 推荐 -->
<#assign results = heavyOperation()>
<#list results as item> - 合理使用缓存:
- spring.freemarker.cache=true
spring.freemarker.settings.template_update_delay=3600 - 减少嵌套深度:
- <#-- 不推荐 -->
<#if a>
<#if b>
<#if c>
...
<#-- 推荐 -->
<#if a && b && c>
...
7.4 安全考虑
<#-- HTML转义 -->
${userInput?html}
<#-- JSON转义 -->
${jsonData?json_string}
<#-- 禁用转义 (谨慎使用) -->
<#noescape>${trustedHtml}</#noescape>
八、FreeMarker 2.3.x 新特性
8.1 方法调用操作符
<#-- 传统方式 -->
${user.getName()}
<#-- 新方式 -->
${user.name()}
8.2 链式调用
${users?filter(u -> u.active)?sort_by("name")?first.name}
8.3 Lambda表达式
<#list users?filter(u -> u.age > 18) as user>
${user.name}
</#list>
<#assign square = x -> x * x>
${square(5)} <#-- 输出25 -->
8.4 集合操作增强
<#-- 直接创建序列 -->
<#assign nums = [1, 2, 3]>
<#-- 直接创建哈希 -->
<#assign map = {"key1": "value1", "key2": "value2"}>
<#-- 集合拼接 -->
<#assign combined = nums + [4, 5]>
九、常见问题解决
9.1 模板找不到错误
错误:TemplateNotFoundException
解决方案:
- 检查模板路径是否正确
- 确认文件后缀匹配配置 (spring.freemarker.suffix)
- 检查模板加载路径 (spring.freemarker.template-loader-path)
9.2 变量未定义错误
错误:InvalidReferenceException
解决方案:
- 使用默认值 ${var!default}
- 检查变量名拼写
- 确认变量已正确添加到模型
9.3 性能问题
现象:模板渲染慢
优化方案:
- 启用模板缓存
- 减少模板复杂度
- 将复杂计算移到Java代码中
- 避免深层嵌套
9.4 日期格式化问题
错误:日期显示不正确
解决方案:
- 明确指定日期格式 ${date?string("yyyy-MM-dd")}
- 配置默认格式:
- spring.freemarker.settings.date_format=yyyy-MM-dd
spring.freemarker.settings.datetime_format=yyyy-MM-dd HH:mm:ss
十、综合示例
10.1 用户信息卡片
<#macro userCard user showDetails=false>
<div class="card ${user.isPremium?then('premium', 'regular')}">
<div class="card-header">
<h3>${user.name}</h3>
<span class="badge ${user.gender?lower_case}">${user.gender?cap_first}</span>
</div>
<div class="card-body">
<p>Email: ${user.email}</p>
<p>Member since: ${user.joinDate?string('yyyy-MM-dd')}</p>
<#if showDetails>
<div class="details">
<p>Address: ${user.address!}</p>
<p>Phone: ${user.phone!'Not provided'}</p>
</div>
</#if>
</div>
<#if user.tags?? && user.tags?size gt 0>
<div class="card-footer">
<#list user.tags as tag>
<span class="tag">${tag}</span>
</#list>
</div>
</#if>
</div>
</#macro>
<#-- 使用宏 -->
<@userCard user=currentUser showDetails=true/>
10.2 分页组件
<#macro pagination paginationData url>
<#if paginationData.totalPages gt 1>
<nav class="pagination">
<#-- 上一页 -->
<#if paginationData.hasPrevious()>
<a href="${url}?page=${paginationData.number}"><< Previous</a>
<#else>
<span class="disabled"><< Previous</span>
</#if>
<#-- 页码 -->
<#list 1..paginationData.totalPages as page>
<#if page == paginationData.number + 1>
<span class="current">${page}</span>
<#else>
<a href="${url}?page=${page}">${page}</a>
</#if>
</#list>
<#-- 下一页 -->
<#if paginationData.hasNext()>
<a href="${url}?page=${paginationData.number + 2}">Next >></a>
<#else>
<span class="disabled">Next >></span>
</#if>
</nav>
</#if>
</#macro>
头条对markdown的文章显示不太友好,想了解更多的可以关注微信公众号:“Eric的技术杂货库”,后期会有更多的干货以及资料下载。