FreeMarker 中的哈希表(Hash)是存储键值对数据的核心结构,类似于 Java 中的 Map
。熟练掌握哈希表的访问是构建动态、数据驱动模板的基础。本指南将提供全面、深入的实践指导。
核心概念
- 哈希表 (Hash):一种数据结构,通过唯一的键(Key)来存储和检索对应的值(Value)。键通常是字符串,值可以是任何 FreeMarker 支持的类型(字符串、数字、布尔值、日期、其他哈希表、序列等)。
- 键 (Key):用于标识哈希表中特定值的唯一标识符,通常为字符串。键是区分大小写的。
- 值 (Value):与键关联的数据,可以是简单类型或复杂对象。
- 点号访问 (
.
):最常用的访问哈希表值的方法,语法为hashName.keyName
。它尝试将keyName
作为哈希表的键查找,如果找不到,则尝试将其解释为哈希表对象的属性(如 Java Bean 的 getter 方法)。 - 方括号访问 (
[]
):另一种访问方法,语法为hashName["keyName"]
或hashName[keyExpression]
。它严格地将括号内的表达式求值后的结果作为键在哈希表中查找,不会尝试访问对象属性。 - 嵌套哈希表 (Nested Hashes):哈希表的值本身也可以是另一个哈希表,形成树状或层级结构。
- 动态键 (Dynamic Keys):使用方括号
[]
时,键可以是一个变量或表达式,其值在运行时确定。 ?keys
和?values
内建函数:分别返回哈希表所有键的序列和所有值的序列。?size
内建函数:返回哈希表中键值对的数量。
操作步骤 (非常详细)
步骤 1: 理解哈希表创建与数据准备
- 目的:明确哈希表数据的来源。
- 详细步骤:
- Java 层创建:最常见的来源。在 Java 代码中创建
Map
或 Java Bean 对象,并将其放入数据模型 (dataModel
)。// 创建一个用户信息 Map Map<String, Object> user = new HashMap<>(); user.put("name", "Alice"); user.put("age", 30); user.put("active", true); // 创建一个地址哈希表 (嵌套) Map<String, Object> address = new HashMap<>(); address.put("street", "123 Main St"); address.put("city", "Beijing"); address.put("zipcode", "100000"); user.put("address", address); // 嵌套 // 创建一个偏好设置哈希表 Map<String, Object> preferences = new HashMap<>(); preferences.put("theme", "dark"); preferences.put("language", "zh-CN"); user.put("preferences", preferences); // 嵌套 // 将 user Map 放入数据模型 Map<String, Object> dataModel = new HashMap<>(); dataModel.put("user", user);
- 模板内创建:使用
assign
指令在模板中直接定义哈希表。<#assign config = { "apiUrl": "https://api.example.com", "timeout": 5000, "debug": false, "features": ["search", "filter", "sort"] }>
- 从其他数据源获取:如 JSON 解析结果、数据库查询结果等,最终都会被转换为 FreeMarker 可识别的哈希表结构。
- Java 层创建:最常见的来源。在 Java 代码中创建
步骤 2: 使用点号 (.
) 访问哈希表值
- 目的:访问已知键名的哈希表值。
- 语法:
hashVariable.keyName
- 详细步骤:
- 访问顶层键值对:
<p>姓名: ${user.name}</p> <!-- 输出: Alice --> <p>年龄: ${user.age}</p> <!-- 输出: 30 --> <p>状态: ${(user.active?string("启用", "禁用"))}</p> <!-- 输出: 启用 -->
- 访问嵌套哈希表:通过链式
.
操作符访问深层结构。<p>街道: ${user.address.street}</p> <!-- 输出: 123 Main St --> <p>城市: ${user.address.city}</p> <!-- 输出: Beijing --> <p>邮编: ${user.address.zipcode}</p> <!-- 输出: 100000 --> <p>主题: ${user.preferences.theme}</p> <!-- 输出: dark --> <p>语言: ${user.preferences.language}</p> <!-- 输出: zh-CN -->
- 处理键名包含特殊字符:如果键名包含空格、连字符
-
、点号.
或其他非字母数字字符,点号访问通常会失败或产生意外结果,因为它可能被解释为属性访问。应优先使用方括号[]
。<#-- 假设有键名为 "first-name" 或 "user info" 的情况 (不推荐,但可能发生) --> <#-- ${user.first-name} 会尝试计算 user.first 减去 name,错误! --> <#-- ${user.user info} 语法错误! -->
- 访问顶层键值对:
步骤 3: 使用方括号 ([]
) 访问哈希表值
- 目的:安全、精确地访问哈希表值,特别是键名含特殊字符或使用动态键时。
- 语法:
hashVariable["keyName"]
或hashVariable[keyExpression]
- 详细步骤:
- 访问含特殊字符的键:
<#-- 假设 user 哈希表有一个键 "first-name" --> <p>名字: ${user["first-name"]}</p> <!-- 正确访问 --> <#-- 假设有一个键 "user info" --> <p>信息: ${user["user info"]}</p> <!-- 正确访问 -->
- 使用变量作为键 (动态键):
- 创建一个变量存储键名。
- 在
[]
中使用该变量。
<#assign settingKey = "theme"> <p>当前设置 (${settingKey}): ${user.preferences[settingKey]}</p> <!-- 输出: 当前设置 (theme): dark --> <#assign langKey = "language"> <p>语言: ${user.preferences[langKey]}</p> <!-- 输出: zh-CN -->
- 使用表达式作为键:
<#-- 假设根据条件选择不同的偏好键 --> <#assign prefType = "display"> <#assign dynamicKey = prefType + "Setting"> <!-- 表达式: "displaySetting" --> <#-- 假设 preferences 哈希表有 "displaySetting" 键 --> <p>显示设置: ${user.preferences[dynamicKey]}</p>
- 访问数字索引的序列 (当哈希表模拟序列时):
- 虽然不常见,但如果哈希表的键是数字字符串,可以用
[]
模拟序列访问。
<#assign numHash = {"0": "zero", "1": "one", "2": "two"}> <p>索引 1: ${numHash["1"]}</p> <!-- 输出: one --> <p>索引 1 (变量): <#assign idx = 1>${numHash[idx?string]}</p> <!-- 注意 idx 需转为字符串 -->
- 虽然不常见,但如果哈希表的键是数字字符串,可以用
- 访问含特殊字符的键:
步骤 4: 遍历哈希表
- 目的:访问哈希表中的所有键值对。
- 语法:
<#list hash as key, value> ... </#list>
- 详细步骤:
- 基本遍历:
<h3>用户信息:</h3> <ul> <#list user as key, value> <li><strong>${key?html}:</strong> <#if value?is_hash> <!-- 如果值是哈希表,递归或特殊处理 --> [对象] <#elseif value?is_sequence> <!-- 如果值是序列 --> ${value?join(", ")} <#else> ${value?html} </#if> </li> </#list> </ul> <!-- 输出类似: - name: Alice - age: 30 - active: true - address: [对象] - preferences: [对象] -->
- 只遍历键:使用
?keys
。<p>可用的用户字段: ${(user?keys?join(", "))}</p> <!-- 输出: name, age, active, address, preferences -->
- 只遍历值:使用
?values
。<p>所有值: ${(user?values)}</p> <!-- 输出所有值,可能包含对象 -->
- 获取哈希表大小:使用
?size
。<p>用户信息包含 ${user?size} 个字段。</p> <!-- 输出: 5 -->
- 基本遍历:
步骤 5: 检查键是否存在与处理 null
- 目的:安全地访问可能不存在的键。
- 详细步骤:
- 使用
??
检查键/值是否存在:<#-- 检查顶层键 --> <#if user.phone??> <p>电话: ${user.phone}</p> <#else> <p>电话: 未提供</p> </#if> <#-- 检查嵌套键 (链式 ??) --> <#if user.address??> <#if user.address.country??> <p>国家: ${user.address.country}</p> <#else> <p>国家: 未填写</p> </#if> </#else> <p>地址信息不完整。</p> </#if>
- 使用
!
提供默认值:<p>电话: ${user.phone!"未提供"}</p> <p>国家: ${user.address.country!"中国"}</p> <!-- 注意: 如果 address 为 null,此表达式会报错! --> <!-- 更安全的方式: --> <p>国家: ${(user.address!{}).country!"中国"}</p> <!-- 先给 address 提供一个空哈希表 {} 作为默认值,再访问其 country -->
- 使用
?? then ... else ...
进行选择:${user.email ?? "邮箱: " + user.email else "暂无邮箱"}
- 使用
步骤 6: 在 Java 代码中渲染模板
- 目的:将包含哈希表访问逻辑的模板与数据结合。
- 详细步骤:
- 配置 FreeMarker:
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31); cfg.setClassForTemplateLoading(YourClass.class, "/templates"); // 模板路径 cfg.setDefaultEncoding("UTF-8");
- 加载模板:
Template template = cfg.getTemplate("user_profile.ftl"); // 模板文件名
- 准备并填充数据模型(如步骤 1 所示)。
- 执行渲染:
Writer out = new OutputStreamWriter(System.out, "UTF-8"); // 或写入文件、HTTP 响应等 template.process(dataModel, out); out.flush();
- 配置 FreeMarker:
常见错误
- 点号访问特殊字符键:
- 错误:
${user.first-name}
(试图计算user.first
减name
)。 - 错误:
${user.user info}
(语法错误)。 - 正确:
${user["first-name"]}
,${user["user info"]}
。
- 错误:
- 混淆
.
和[]
的行为:- 错误假设:认为
hash.key
和hash["key"]
在所有情况下都等价。 - 现实:
hash.key
会先查哈希表键,查不到会尝试getKey()
方法;hash["key"]
只查哈希表键。当哈希表来自 Java Bean 且有同名 getter 时,行为可能不同。
- 错误假设:认为
- 访问
null
哈希表的键:- 错误:
${user.address.country}
当user.address
为null
时,会抛出异常。 - 正确:使用
??
或!
:${(user.address!{}).country!"未知"}
或<#if user.address??>...<#if user.address.country??>...</#if></#if>
。
- 错误:
- 动态键未正确求值:
- 错误:
<#assign keyVar = "theme">${user.preferences.keyVar}
— 这会尝试访问名为keyVar
的键,而不是theme
。 - 正确:
${user.preferences[keyVar]}
。
- 错误:
- 遍历时未处理复杂值类型:在
<#list>
中直接输出value
,当value
是哈希表或序列时,可能得到[Object object]
或错误。- 正确:使用
?is_hash
,?is_sequence
等内建函数进行类型检查和相应处理。
- 正确:使用
- 键名大小写敏感:
user.Name
和user.name
被视为不同的键。 - 在
[]
中使用未定义变量:${user[undefinedVar]}
会报错,因为undefinedVar
不存在。
注意事项
- 键的类型:虽然键通常是字符串,但 FreeMarker 也支持其他类型(如数字)。但在实际应用中,字符串键最普遍。
- 性能:点号
.
和方括号[]
的访问性能都非常高,通常不是瓶颈。遍历大型哈希表的性能主要取决于循环体内的操作。 - 可读性:
- 对于简单、标准的键名(字母、数字、下划线),优先使用
.
,更简洁。 - 对于含特殊字符或动态键,必须使用
[]
。
- 对于简单、标准的键名(字母、数字、下划线),优先使用
null
值 vsnull
键:hash["key"]
返回null
:表示键"key"
存在,但其值为null
。hash["nonExistentKey"]
返回null
:表示键"nonExistentKey"
不存在。??
操作符在这两种情况下都返回false
,因为它检查的是表达式整体是否为null
。要区分键是否存在和值是否为null
,需要更复杂的逻辑或在 Java 层处理。
- 作用域:在
<#list hash as key, value>
中,key
和value
是循环变量,作用域仅限于循环体内。 ?keys
和?values
的顺序:它们返回的序列顺序不保证是哈希表插入的顺序(取决于底层Map
实现,如LinkedHashMap
保证顺序,HashMap
不保证)。如果需要有序,应在 Java 层使用LinkedHashMap
或在模板中对?keys
序列进行排序 (?sort
)。
使用技巧
- 安全的嵌套访问:使用
!{}
为中间哈希表提供空哈希表默认值。${(user.address!{}).zipcode!"未知邮编"}
- 动态配置访问:利用
[]
和变量轻松访问配置项。<#assign env = "prod"> <!-- 或 "dev" --> <#assign apiUrl = config[env + "ApiUrl"]> <!-- config.devApiUrl 或 config.prodApiUrl -->
- 使用
?keys
进行条件检查:<#if "admin" in user.roles?keys> <!-- 假设 roles 是一个哈希表 --> 显示管理员功能 </#if>
- 结合
?index_of
检查键:虽然?keys
更直接,但?index_of
也可用。<#if user?keys?index_of("phone") != -1> 有电话号码 </#if>
- 在宏中传递哈希表:将哈希表作为参数传递给宏,实现代码复用。
<#macro displayAddress addrHash> <address> ${addrHash.street}<br> ${addrHash.city}, ${addrHash.zipcode} </address> </#macro> <@displayAddress addrHash=user.address />
最佳实践与性能优化
- 优先使用
.
访问简单键:代码更简洁易读。 - 强制使用
[]
访问特殊键或动态键:确保正确性和可预测性。 - 防御性编程,无处不在的
??
和!
:在访问任何可能为null
的哈希表或其键之前,进行防护。 - 在 Java 层进行数据预处理:
- 将复杂的数据结构扁平化或转换为更适合模板展示的形式。
- 提前计算好模板中需要的标志位(如
isPremium
,hasDiscount
)。 - 使用
LinkedHashMap
保证需要有序输出的哈希表的顺序。
- 避免在循环中进行昂贵操作:遍历大型哈希表时,确保循环体内的操作(如复杂计算、数据库调用——虽然模板中不应有)尽可能轻量。
- 重用
Configuration
,缓存Template
:这是 FreeMarker 性能优化的根本。Configuration
是线程安全的,应全局单例;Template
对象会被Configuration
缓存。 - 保持哈希表结构清晰:设计合理的数据模型,避免过深的嵌套或过于复杂的结构,便于模板访问。
- 使用有意义的键名:使用清晰、一致的命名约定(如
snake_case
,camelCase
),避免特殊字符。 - 利用
?size
进行空检查:<#if (hash?size > 0)>
可以检查哈希表是否非空。 - 文档化数据模型:如果模板使用的数据结构复杂,提供文档说明键的含义和类型。