FreeMarker 语法全面详解

FreeMarker 语法全面详解

技术教程gslnedu2025-06-08 17:31:061A+A-

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 运算符优先级

  1. ? ! ??
  2. [] .
  3. - + (一元)
  4. * / %
  5. + - (二元)
  6. < > <= >=
  7. == !=
  8. &&
  9. ||

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 性能优化

  1. 避免在模板中计算
  2. <#-- 不推荐 -->
    <#list heavyOperation() as item>

    <#-- 推荐 -->
    <#assign results = heavyOperation()>
    <#list results as item>
  3. 合理使用缓存
  4. spring.freemarker.cache=true
    spring.freemarker.settings.template_update_delay=3600
  5. 减少嵌套深度
  6. <#-- 不推荐 -->
    <#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

解决方案

  1. 检查模板路径是否正确
  2. 确认文件后缀匹配配置 (spring.freemarker.suffix)
  3. 检查模板加载路径 (spring.freemarker.template-loader-path)

9.2 变量未定义错误

错误InvalidReferenceException

解决方案

  1. 使用默认值 ${var!default}
  2. 检查变量名拼写
  3. 确认变量已正确添加到模型

9.3 性能问题

现象:模板渲染慢

优化方案

  1. 启用模板缓存
  2. 减少模板复杂度
  3. 将复杂计算移到Java代码中
  4. 避免深层嵌套

9.4 日期格式化问题

错误:日期显示不正确

解决方案

  1. 明确指定日期格式 ${date?string("yyyy-MM-dd")}
  2. 配置默认格式:
  3. 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的技术杂货库”,后期会有更多的干货以及资料下载。

点击这里复制本文地址 以上内容由朽木教程网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

朽木教程网 © All Rights Reserved.  蜀ICP备2024111239号-8