FreeMarker 中的序列(Sequence)是存储有序元素集合的核心数据结构,类似于 Java 中的 List 或数组。对序列进行操作是模板开发中的常见需求,如遍历列表、访问元素、检查内容等。本文将系统介绍 FreeMarker 的序列操作,涵盖核心概念、详细操作步骤、常见错误、注意事项、使用技巧以及最佳实践与性能优化。
1. 核心概念
什么是序列 (Sequence)
- 定义:序列是 FreeMarker 中一种有序的、可索引的数据结构,用于存储多个值。元素按插入顺序排列,并可通过索引(从 0 开始)访问。
- 类比:类似于 Java 的
List
、Set
(当传递给模板时通常视为序列)、数组(Object[]
,String[]
等)。 - 创建方式:
- 在 Java 代码中创建
List
、数组等对象并放入数据模型。 - 在模板中使用
[...]
语法创建(较少用,主要用于常量或简单组合)。
- 在 Java 代码中创建
序列的关键特性
- 有序性:元素有固定的顺序。
- 可索引:可通过数字索引访问特定位置的元素(
sequence[0]
)。 - 可变长度:序列的长度可以是任意的(包括 0,即空序列)。
- 元素类型:序列中的元素可以是任何 FreeMarker 支持的类型(字符串、数字、哈希表、其他序列等),且同一个序列中元素类型可以不同。
主要操作类型
- 遍历 (Iteration):使用
<#list>
指令逐个处理序列中的元素。 - 访问 (Access):通过索引获取特定元素。
- 检查 (Checking):判断序列是否为空、是否包含元素、获取长度等。
- 切片 (Slicing):获取序列的子集。
- 内建函数 (Built-ins):FreeMarker 提供了一系列内建函数来操作序列。
2. 操作步骤(非常详细)
以下是使用 FreeMarker 序列操作的详细步骤,帮助您快速掌握并实践。
步骤 1:准备开发环境
引入 FreeMarker 依赖:
- Maven (
pom.xml
):<dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.32</version> </dependency>
- Gradle (
build.gradle
):implementation 'org.freemarker:freemarker:2.3.32'
- Maven (
创建模板文件:
- 在
src/main/resources/templates
目录下创建sequence_operations.ftl
。
- 在
步骤 2:定义数据模型(Java 代码)
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import java.io.*;
import java.util.*;
public class SequenceOperationsExample {
public static void main(String[] args) throws IOException, TemplateException {
// 1. 创建 FreeMarker 配置
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setClassForTemplateLoading(SequenceOperationsExample.class, "/templates");
// 2. 获取模板
Template template = cfg.getTemplate("sequence_operations.ftl");
// 3. 创建数据模型
Map<String, Object> dataModel = new HashMap<>();
// 创建用户列表 (List<User>)
List<Map<String, Object>> users = new ArrayList<>();
Map<String, Object> user1 = new HashMap<>();
user1.put("id", 1);
user1.put("name", "Alice");
user1.put("age", 30);
user1.put("active", true);
user1.put("tags", Arrays.asList("developer", "java")); // 嵌套序列
Map<String, Object> user2 = new HashMap<>();
user2.put("id", 2);
user2.put("name", "Bob");
user2.put("age", 25);
user2.put("active", false);
user2.put("tags", Arrays.asList("designer", "ui")); // 嵌套序列
Map<String, Object> user3 = new HashMap<>();
user3.put("id", 3);
user3.put("name", "Charlie");
user3.put("age", 35);
user3.put("active", true);
user3.put("tags", Arrays.asList("manager")); // 嵌套序列
users.add(user1);
users.add(user2);
users.add(user3);
dataModel.put("users", users);
// 创建一个简单的字符串数组
String[] colors = {"Red", "Green", "Blue"};
dataModel.put("colors", colors);
// 创建一个空序列
List<String> emptyList = Collections.emptyList();
dataModel.put("emptyItems", emptyList);
// 创建一个数字序列
List<Integer> numbers = Arrays.asList(10, 20, 5, 15, 30);
dataModel.put("numbers", numbers);
// 4. 准备输出
Writer out = new OutputStreamWriter(System.out);
// 5. 合并模板与数据
template.process(dataModel, out);
// 6. 清理
out.flush();
out.close();
}
}
步骤 3:编写模板文件(sequence_operations.ftl
)
<#-- 序列操作示例 -->
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>FreeMarker 序列操作演示</title>
<style>
table, th, td { border: 1px solid #ddd; border-collapse: collapse; padding: 8px; }
th { background-color: #f2f2f2; }
.inactive { color: gray; }
</style>
</head>
<body>
<h1>序列操作演示</h1>
<#-- 1. 遍历序列 (<#list>) -->
<h2>1. 用户列表</h2>
<#-- 检查序列是否存在且有内容 -->
<#if users?has_content>
<table>
<thead>
<tr>
<th>#</th>
<th>ID</th>
<th>姓名</th>
<th>年龄</th>
<th>状态</th>
<th>标签</th>
</tr>
</thead>
<tbody>
<#-- 使用 <#list> 遍历 users 序列 -->
<#list users as user>
<tr class="${user.active?then('', 'inactive')}">
<td>${user_index + 1}</td> <!-- user_index 是 FreeMarker 提供的循环变量,从 0 开始 -->
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.age}</td>
<td>${user.active?string("活跃", "非活跃")}</td>
<td>
<#-- 遍历嵌套序列 tags -->
<#if user.tags?has_content>
<#list user.tags as tag>
<span style="background:lightblue; padding:2px 4px; margin:0 2px;">${tag}</span>
</#list>
<#else>
无标签
</#if>
</td>
</tr>
</#list>
</tbody>
</table>
<p>共 ${users?size} 位用户。</p>
<#else>
<p>暂无用户信息。</p>
</#if>
<#-- 2. 访问序列元素 (通过索引) -->
<h2>2. 访问特定元素</h2>
<#-- 检查索引是否有效 -->
<#if users?size > 0>
<p>第一位用户: ${users[0].name} (ID: ${users[0].id})</p>
<#else>
<p>无法访问,用户列表为空。</p>
</#if>
<#-- 安全访问 (避免索引越界) -->
<#-- 方法1: 先检查长度 -->
<#assign lastIndex = users?size - 1>
<#if lastIndex >= 0>
<p>最后一位用户: ${users[lastIndex].name}</p>
<#else>
<p>用户列表为空。</p>
</#if>
<#-- 方法2: 使用 try/catch (FreeMarker 支持) -->
<#try>
<p>尝试访问第 5 位用户: ${users[4].name}</p>
<#catch>
<p>错误:无法访问第 5 位用户(索引越界或列表为空)。</p>
</#try>
<#-- 3. 序列切片 (Slicing) -->
<h2>3. 序列切片</h2>
<#-- 获取前两位用户 -->
<#if users?size >= 2>
<h3>前两位用户:</h3>
<ul>
<#list users[0..1] as user> <!-- 包含索引 0 和 1 -->
<li>${user.name}</li>
</#list>
</ul>
<#else>
<p>用户不足两位。</p>
</#if>
<#-- 获取从第二位开始到最后的所有用户 -->
<#if users?size > 1>
<h3>从第二位开始的用户:</h3>
<ul>
<#list users[1..] as user> <!-- 从索引 1 到末尾 -->
<li>${user.name}</li>
</#list>
</ul>
<#else>
<p>用户不足两位或为空。</p>
</#if>
<#-- 4. 使用内建函数 (Built-ins) -->
<h2>4. 内建函数操作</h2>
<#-- ?size: 获取序列长度 -->
<p>颜色数量: ${colors?size}</p>
<#-- ?first: 获取第一个元素 -->
<#if colors?has_content>
<p>第一种颜色: ${colors?first}</p>
<#else>
<p>颜色列表为空。</p>
</#if>
<#-- ?last: 获取最后一个元素 -->
<#if colors?has_content>
<p>最后一种颜色: ${colors?last}</p>
<#else>
<p>颜色列表为空。</p>
</#if>
<#-- ?sort: 排序 (升序) -->
<#if numbers?has_content>
<p>排序前的数字: ${numbers?join(", ")}</p>
<p>升序排序: ${numbers?sort?join(", ")}</p>
<p>降序排序: ${numbers?sort_by("-.")?join(", ")} <!-- 使用 - 表示降序,. 代表元素自身 --></p>
</#if>
<#-- ?join: 将序列元素连接成字符串 -->
<#if colors?has_content>
<p>颜色列表: ${colors?join(" | ")}</p>
</#if>
<#-- ?seq_contains: 检查序列是否包含指定元素 -->
<#if users?seq_contains(users[0])> <!-- 检查是否包含第一个用户对象 -->
<p>用户列表包含 Alice。</p>
<#else>
<p>用户列表不包含 Alice。</p>
</#if>
<#-- 注意:?seq_contains 通常用于检查简单值 -->
<#assign userNames = users?map(user -> user.name)> <!-- 先提取名字序列 -->
<#if userNames?seq_contains("Bob")>
<p>存在名为 Bob 的用户。</p>
</#if>
<#-- 5. 处理空序列 -->
<h2>5. 空序列处理</h2>
<#-- ?has_content 是检查空序列的推荐方法 -->
<#if emptyItems?has_content>
<p>非空序列。</p>
<#else>
<p>这是一个空序列或 null。</p>
</#if>
<#-- ?size 为 0 也表示空 -->
<#if emptyItems?size == 0>
<p>序列长度为 0。</p>
</#if>
<#-- 6. 在模板中创建简单序列 (不常用) -->
<h2>6. 模板内创建序列</h2>
<#-- 使用 [...] 语法 -->
<#assign weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]>
<#list weekdays as day>
<p>${day_index + 1}. ${day}</p>
</#list>
<#-- 7. 使用 ?map, ?filter 进行转换和过滤 (高级) -->
<h2>7. 序列转换与过滤</h2>
<#-- ?map: 将序列中的每个元素转换为新值 -->
<#assign userAges = users?map(user -> user.age)>
<p>用户年龄: ${userAges?join(", ")}</p>
<#-- ?filter: 过滤出满足条件的元素 -->
<#assign activeUsers = users?filter(user -> user.active)>
<h3>活跃用户:</h3>
<ul>
<#list activeUsers as user>
<li>${user.name} (年龄: ${user.age})</li>
</#list>
</ul>
<#-- ?map 和 ?filter 结合 -->
<#assign activeUserNames = users?filter(user -> user.active)?map(user -> user.name)>
<p>活跃用户姓名: ${activeUserNames?join(", ")}</p>
</body>
</html>
步骤 4:运行程序并查看结果
- 编译并运行
SequenceOperationsExample.java
。 - 查看控制台输出的 HTML 内容,验证序列的各种操作(遍历、访问、切片、内建函数等)是否按预期工作。
3. 常见错误
索引越界 (Index Out of Bounds):
- 错误:
${users[10].name}
,当users
序列长度小于 11 时。 - 原因:试图访问不存在的索引位置。
- 解决:
- 在访问前使用
?size
检查序列长度。 - 使用
<#try>
/<#catch>
捕获异常。 - 优先使用
<#list>
遍历,避免硬编码索引。
- 在访问前使用
- 错误:
访问
null
序列或元素:- 错误:
${userList[0].name}
,如果userList
本身为null
。 - 原因:序列变量为
null
。 - 解决:
- 使用
??
操作符提供默认值:${(userList??[])[0].name}
(如果userList
为null
,则使用空列表[]
)。 - 在
<#if>
中先检查?exists
或??
。
- 使用
- 错误:
混淆
?has_content
和??
:- 问题:使用
??
检查空序列。 - 示例:
<#if users??>
只能判断users
是否为null
,但无法判断一个非null
的空列表[]
。 - 解决:始终使用
?has_content
来检查序列是否“有内容”(非null
且长度 > 0)。??
仅用于检查null
。
- 问题:使用
在
<#list>
外部错误使用循环变量:- 错误:在
<#list>
循环结束后,尝试使用user
、user_index
等变量。 - 原因:这些变量仅在
<#list>
指令的内部作用域内有效。 - 解决:如果需要在循环外使用某个值,应在循环内使用
<#assign>
将其赋值给一个外部变量。
- 错误:在
对非序列类型使用序列操作:
- 错误:
${"hello"[0]}
或<#list "hello" as char>
。 - 原因:字符串不是序列,不能像数组一样通过索引访问单个字符(FreeMarker 2.3.21+ 支持
?string
相关操作,但?get
不是标准序列操作),也不能直接用<#list>
遍历。 - 解决:如果需要处理字符串中的字符,通常需要在 Java 后端将其转换为字符数组或字符串列表,再传递给模板。
- 错误:
?sort
的误解:- 问题:认为
?sort
会修改原序列。 - 解决:
?sort
返回一个新的排序后的序列,原序列不变。需要将结果赋值给新变量或直接使用。
- 问题:认为
4. 注意事项
?has_content
是金标准:- 对于序列(和字符串),
?has_content
是最安全、最常用的检查“非空”的方法。它同时检查null
和长度为 0 的情况。
- 对于序列(和字符串),
循环变量的作用域:
<#list sequence as item>
中的item
、item_index
、item_has_next
等变量仅在<#list>
和</#list>
之间有效。
?first
和?last
的安全性:- 当序列为空时,
${sequence?first}
或${sequence?last}
会抛出异常。 - 解决:务必先用
<#if sequence?has_content>
检查,或使用<#try>
/<#catch>
。
- 当序列为空时,
?join
的分隔符:?join(separator)
用指定的分隔符连接序列元素。如果序列为空,结果为空字符串""
。
?map
和?filter
的 Lambda 表达式:- 这些是 FreeMarker 2.3.23+ 引入的高级功能。
- 语法:
sequence?map(item -> item.property)
或sequence?filter(item -> item.age > 18)
。 ->
左边是参数(元素),右边是表达式。
性能考虑:
<#list>
遍历是 O(n) 操作,对于非常大的序列,渲染时间会增加。?map
、?filter
、?sort
等操作也会遍历序列,产生新的序列对象,消耗内存和 CPU。
5. 使用技巧
结合
<#assign>
和内建函数:<#-- 提取活跃用户列表 --> <#assign activeUsers = users?filter(user -> user.active)> <#-- 计算平均年龄 --> <#assign totalAge = 0> <#list users as user><#assign totalAge = totalAge + user.age></#list> <#if users?has_content> <p>平均年龄: ${(totalAge / users?size)?string("0.##")}</p> </#if>
使用
?join
格式化输出:<#-- 创建逗号分隔的标签列表 --> <#if user.tags?has_content> 标签: ${user.tags?join(", ")} </#if>
利用
?sort_by
进行复杂排序:<#-- 按年龄升序排序 --> <#list users?sort_by("age") as user> ... </#list> <#-- 按姓名降序排序 --> <#list users?sort_by("-name") as user> ... </#list> <#-- 先按状态降序,再按年龄升序 --> <#list users?sort_by(["-active", "age"]) as user> ... </#list>
?chunk
分组显示:- 将序列分割成固定大小的子序列。
- 示例:
<#list users?chunk(2) as userRow>
,然后在内层<#list userRow as user>
遍历每行的两个用户,常用于表格布局。
安全访问嵌套序列:
<#-- 安全地访问用户的第一种标签 --> <#if (user.tags??) && (user.tags?size > 0)> 第一个标签: ${user.tags[0]} </#if>
6. 最佳实践与性能优化
数据预处理(推荐):
- 最佳实践:在 Java 后端完成复杂的序列操作,如排序、过滤、分组、聚合计算等。将处理好的、适合展示的数据结构(如已排序的列表、分页后的列表、计算好的统计数据)传递给模板。
- 优点:模板逻辑简单清晰;后端逻辑易于测试和优化;通常性能优于在模板引擎中执行;避免模板中出现大量
?map
/?filter
。
避免在模板中进行昂贵操作:
- 不要在
<#list>
循环内部执行复杂的计算或调用开销大的宏。 - 避免对大序列进行
?sort
、?filter
等操作,除非必要且数据量小。
- 不要在
合理使用缓存:
- 确保
Configuration
被正确配置和重用,利用模板缓存。 - 对于计算结果稳定且使用频繁的序列处理(如静态的选项列表),可以在 Java 层缓存处理后的结果。
- 确保
分页处理大数据集:
- 如果需要展示的序列非常大(如成千上万条记录),绝对不要一次性加载到内存并传递给模板进行
<#list>
遍历。 - 优化:在后端实现分页查询,每次只获取一页数据(如 10-50 条)传递给模板。
- 如果需要展示的序列非常大(如成千上万条记录),绝对不要一次性加载到内存并传递给模板进行
使用
?chunk
优化布局:- 当需要将列表数据按行或按组展示时(如每行显示 3 个商品),使用
?chunk(N)
可以简化模板逻辑,比手动计算索引更清晰。
- 当需要将列表数据按行或按组展示时(如每行显示 3 个商品),使用
监控与分析:
- 关注模板渲染时间,特别是包含大型序列操作的模板。
- 使用性能分析工具识别瓶颈,优先考虑将耗时操作移至后端。
通过以上系统性的介绍和详细的实践步骤,您应该能够全面掌握 FreeMarker 序列操作的核心知识,并在实际项目中高效、安全地应用,同时避免常见陷阱,遵循最佳实践以构建高性能的模板系统。