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)>可以检查哈希表是否非空。 - 文档化数据模型:如果模板使用的数据结构复杂,提供文档说明键的含义和类型。