FreeMarker 提供了简洁高效的条件运算符,用于处理 null
值、提供默认值和进行条件判断。掌握这些运算符是编写健壮、可维护模板的关键。本指南将深入解析。
核心概念
null
值 (Null Value):表示变量未定义或值为空。直接访问null
对象的属性或方法会导致TemplateException
。??
运算符 (Existence Operator):- 功能:检查其左侧的表达式是否“存在”(即不为
null
)。 - 返回值:布尔值 (
true
或false
)。 - 用途:常用于
<#if>
条件判断中。
- 功能:检查其左侧的表达式是否“存在”(即不为
!
运算符 (Default Value Operator):- 功能:如果其左侧的表达式为
null
,则返回其右侧指定的默认值;否则返回左侧表达式的值。 - 返回值:与左侧表达式或右侧默认值同类型。
- 用途:安全地访问可能为
null
的变量,并提供后备值。
- 功能:如果其左侧的表达式为
?? then ... else ...
运算符 (Ternary-like Operator):- 功能:这是一个三元运算符的变体。它首先计算
??
左侧的表达式。如果该表达式存在(不为null
),则返回then
后面的表达式;否则返回else
后面的表达式。 - 语法:
expression ?? then_value else else_value
- 返回值:
then_value
或else_value
的值。 - 用途:根据左侧表达式是否存在来选择不同的值或执行不同的逻辑。
- 功能:这是一个三元运算符的变体。它首先计算
- 短路求值 (Short-circuit Evaluation):
??
、!
、?? then ... else ...
都支持短路求值。例如,在a ?? b
中,如果a
存在,则不会计算b
;在a!b
中,如果a
不为null
,则不会使用b
;在a ?? x else y
中,如果a
存在,则不会计算y
。
操作步骤 (非常详细)
步骤 1: 使用 ??
检查变量是否存在
- 目的:确定一个变量或表达式是否有值(不为
null
)。 - 语法:
expression ??
- 详细步骤:
- 识别潜在的
null
变量:确定模板中哪些数据可能来自外部(如数据库、API),可能为null
。例如:user
,user.profile
,user.profile.avatarUrl
,order.discountCode
。 - 在
<#if>
中使用??
:这是最常见的用法。<#-- 检查 user 对象是否存在 --> <#if user??> <p>用户: ${user.name}</p> <#else> <p>用户未登录或信息不可用。</p> </#if>
- 检查嵌套属性:可以链式使用
??
检查深层嵌套的对象。<#-- 检查 profile 和 avatarUrl 是否都存在 --> <#if user.profile??> <!-- 先检查 user.profile --> <#if user.profile.avatarUrl??> <!-- 再检查 avatarUrl --> <img src="${user.profile.avatarUrl}" alt="Avatar"> <#else> <img src="/default-avatar.png" alt="Default Avatar"> </#if> <#else> <img src="/default-avatar.png" alt="Default Avatar"> </#if>
- 结合逻辑运算符:
??
返回布尔值,可以与其他条件组合。<#if user?? && user.isActive> 欢迎回来, ${user.name}! </#if>
- 识别潜在的
步骤 2: 使用 !
提供默认值
- 目的:安全地访问变量,当其为
null
时提供一个替代值。 - 语法:
expression!default_value
或expression!
(右侧无值时默认为空字符串""
)。 - 详细步骤:
- 确定默认值:思考当变量为
null
时,显示什么内容最合适。例如:空字符串""
、"N/A"
、0
、false
、一个默认对象或 URL。 - 在插值
${...}
中使用!
:这是最直接的应用场景,避免因null
导致渲染失败。<#-- 安全访问用户名 --> <p>姓名: ${user.name!"未知用户"}</p> <#-- 安全访问头像 URL --> <img src="${user.profile.avatarUrl!"/images/default.png"}" alt="头像"> <#-- 安全访问数字 --> <p>积分: ${(user.points!0)}</p> <!-- 如果 points 为 null,显示 0 --> <#-- 安全访问布尔值 --> <#assign isPremium = user.membership.type!"basic" == "premium"> <#if isPremium> 尊贵的会员 </#if>
- 在
assign
中使用!
:将带默认值的结果赋给新变量。<#assign displayName = user.nickname!user.username!"Anonymous"> <!-- 优先使用 nickname,其次 username,最后 "Anonymous" --> <p>显示名: ${displayName}</p>
- 在比较中使用
!
:确保比较操作不会因null
而中断。<#-- 安全比较年龄 --> <#if (user.age!0) >= 18> 成年 <#else> 未成年 </#if>
- 使用
!
无右侧值:如果!
后面没有指定值,则默认为""
(空字符串)。${user.bio!} <!-- 如果 bio 为 null,输出空字符串 -->
- 确定默认值:思考当变量为
步骤 3: 使用 ?? then ... else ...
进行条件选择
- 目的:根据左侧表达式是否存在,选择执行两个不同分支中的一个。
- 语法:
expression ?? then_expression else else_expression
- 详细步骤:
- 理解工作原理:
- 首先计算
expression
。 - 如果
expression
存在(不为null
),则计算并返回then_expression
的值。 - 如果
expression
不存在(为null
),则计算并返回else_expression
的值。 - 关键:
then_expression
和else_expression
是表达式,它们的值会被返回或计算。
- 首先计算
- 在插值
${...}
中使用:根据存在性返回不同的字符串或值。${user.email ?? "邮箱: " + user.email else "邮箱未提供"} <!-- 如果 user.email 存在,输出 "邮箱: xxx@xx.com";否则输出 "邮箱未提供" -->
- 在
<#if>
条件中使用:虽然不常见,但可以利用其返回的布尔值或其他值作为条件。<#-- 假设 hasPermission 返回布尔值 --> <#if user.role ?? hasPermission(user.role, "edit") else false> 显示编辑按钮 </#if>
- 在
assign
中使用:将条件选择的结果赋给变量。<#assign avatarUrl = user.profile.avatarUrl ?? user.profile.avatarUrl else "/default-avatar.jpg"> <!-- 等价于更复杂的 if-else,但更简洁 --> <img src="${avatarUrl}" alt="头像">
- 与
!
的区别:!
只提供一个默认值(当左侧为null
时使用右侧)。?? then ... else ...
则提供了两个分支:当左侧存在时执行then
分支,不存在时执行else
分支。!
更侧重于“补救”,而?? then ... else ...
更侧重于“选择”。
- 理解工作原理:
步骤 4: 组合使用 ??
, !
, ?? then ... else ...
- 目的:处理复杂的
null
场景和条件逻辑。 - 详细步骤:
- 链式
!
:为嵌套对象提供多级默认值。<#assign configValue = appConfig.database.url!"localhost:3306"!"/backup/db"> <!-- 如果 appConfig 为 null,则第一个 ! 生效,但第二个 ! 无效(因为 "localhost:3306" 不是 null) --> <!-- 更好的方式是分步或使用 ?? -->
??
与!
结合:先用??
判断,再用!
提供默认值。<#if user.profile?? && user.profile.theme??> <#-- theme 存在,但值可能为 null --> <#assign theme = user.profile.theme!"light"> <#else> <#assign theme = "light"> </#if>
?? then ... else ...
包含复杂表达式:then
和else
分支可以是复杂的表达式,包括函数调用、其他运算符等。${user.loginCount ?? formatCount(user.loginCount) else "首次登录"} <!-- 假设 formatCount 是一个自定义函数 -->
- 链式
步骤 5: 在 Java 代码中传递数据并渲染
- 目的:确保 Java 层数据与模板中的条件处理逻辑匹配。
- 详细步骤:
- 传递可能为
null
的数据:Java 对象中的字段可能为null
。User user = new User(); user.setName("Alice"); // user.setEmail(null); // email 可能为 null user.setProfile(new Profile()); // user.getProfile().setAvatarUrl(null); // avatarUrl 可能为 null Map<String, Object> dataModel = new HashMap<>(); dataModel.put("user", user);
- 传递布尔标志:有时在 Java 层计算复杂条件,传递布尔值给模板,简化模板逻辑。
boolean isEligibleForDiscount = user != null && user.getAge() >= 60 && user.getOrders().size() > 5; dataModel.put("isEligibleForDiscount", isEligibleForDiscount);
<#if isEligibleForDiscount> 您有资格享受老年优惠! </#if>
- 传递可能为
常见错误
- 混淆
??
和!
:- 错误:
${user.name ?? "Unknown"}
—??
返回布尔值,这里试图将其与字符串拼接,通常不是期望的结果。 - 正确:
${user.name!"Unknown"}
(使用!
) 或<#if user.name??>${user.name}<#else>Unknown</#if>
。
- 错误:
- 在
??
后错误使用=
:${user.name ?? "Unknown"}
是错误的,??
不是赋值。 ?? then ... else ...
语法错误:- 错误:
${user.email ?? "Provided" else "Not Provided"}
— 缺少then
关键字。 - 正确:
${user.email ?? "Provided" else "Not Provided"}
是错误的!正确是${user.email ?? "Provided" else "Not Provided"}
。注意:FreeMarker 的?? then ... else ...
语法是left ?? then right1 else right2
。上面的例子如果想根据存在性返回不同字符串,应该是:${user.email ?? "邮箱已提供" else "邮箱未提供"} <!-- 正确 --> <!-- 或者,如果想用 email 的值 --> ${user.email ?? user.email else "未提供"} <!-- 正确,但冗余 -->
- 错误:
- 过度依赖
!
而忽略业务逻辑:盲目使用!
提供默认值,可能导致隐藏数据缺失的问题,应在必要时进行显式检查(??
)并给出明确提示。 - 嵌套
??
过深:连续使用多个??
检查深层嵌套,代码可读性差。- 改进:考虑在 Java 层提供更扁平或更安全的数据结构,或使用
?has_content
(对于集合/字符串)。
- 改进:考虑在 Java 层提供更扁平或更安全的数据结构,或使用
?? then ... else ...
中then
表达式未被计算:误解了?? then ... else ...
的行为。then
后面的表达式只有在左侧存在时才被计算和返回。- 在
!
后放置可能为null
的表达式:a!(b!c)
— 如果a
为null
,则使用b!c
,但如果b
也为null
,则最终是c
。逻辑可能复杂,需仔细设计。
注意事项
null
是核心:所有这些运算符都围绕处理null
值展开。理解数据源中null
的可能性是正确使用它们的前提。- 短路求值:利用短路求值可以避免不必要的计算或潜在的错误(如访问
null
对象的属性)。 !
的默认值:expression!
等价于expression!""
,即默认为空字符串。对于非字符串类型,可能需要显式指定默认值(如!0
,!false
)。?? then ... else ...
的then
关键字:非常重要!必须包含then
关键字。语法是left ?? then right1 else right2
。left ?? right1 else right2
是错误的(在标准 FreeMarker 语法中)。- 性能:这些运算符本身非常轻量级。性能瓶颈通常不在于此,而在于复杂的数据获取或循环。
- 可读性:虽然
?? then ... else ...
很强大,但过于复杂的表达式会降低可读性。适时拆分为assign
变量或使用传统的<#if>
结构。 - 与
?default
内建函数的关系:?default(value)
内建函数功能上等同于!value
。例如,user.name?default("Unknown")
等价于user.name!"Unknown"
。!
语法更简洁。 - 作用域:这些运算符不影响变量作用域。
使用技巧
- 优先使用
!
进行安全访问:在${}
中输出变量时,养成使用!
提供默认值的习惯。 - 使用
??
进行存在性检查:在需要根据是否存在执行不同逻辑块时(如显示/隐藏元素),使用??
配合<#if>
。 ?? then ... else ...
实现简洁的三元选择:当需要根据存在性返回两个不同值时,它是<#if>
的简洁替代。<!-- 简洁 --> ${user.avatar ?? user.avatar else "/default.png"} <!-- 等价但 verbose --> <#if user.avatar??> ${user.avatar} <#else> /default.png </#if>
- 结合内建函数:
?has_content
:对于字符串和集合,比??
更语义化(检查非空)。<#if user.comments?has_content> <!-- 显示评论列表 --> </#if>
?if_exists
:仅当变量存在时才输出其值,否则输出空字符串。${user.name?if_exists}
等价于${user.name!}
。
- 在宏中使用:将常见的
null
处理模式封装成宏。<#macro safeDisplay value defaultMsg="N/A"> ${value!defaultMsg} </#macro> <@safeDisplay value=user.email defaultMsg="未填写邮箱"/>
最佳实践与性能优化
- 防御性编程:始终假设传入模板的数据可能包含
null
值。在访问任何属性前,使用??
或!
进行防护。 - 明确意图:
- 使用
!
当您想提供一个后备值。 - 使用
??
当您想根据存在性做逻辑分支。 - 使用
?? then ... else ...
当您想根据存在性****选择返回两个不同值。
- 使用
- 简化模板逻辑:复杂的业务规则和条件判断应尽量在 Java 代码中完成,传递清晰、预处理过的数据(如布尔标志、计算好的字符串)到模板。模板应专注于展示。
- 避免过度嵌套:减少
??
或<#if>
的嵌套层级,提高可读性。可以提取中间变量 (<#assign>
)。 - 利用
?has_content
:对于集合和字符串的“非空”检查,优先使用?has_content
,语义更清晰。 - 重用
Configuration
,缓存Template
:这是 FreeMarker 性能优化的基石,与运算符无关但至关重要。 - 保持表达式简洁:避免在单个
${}
中构建过长或过于复杂的?? then ... else ...
表达式。拆分有助于维护。 - 测试
null
场景:编写测试用例,确保当关键数据为null
时,模板能正确降级显示(如显示默认图片、提示信息)。 - 文档化默认值:如果使用了重要的默认值,可以在模板注释中说明,便于维护。
- 版本一致性:确认团队使用的 FreeMarker 版本,确保语法(特别是
?? then ... else ...
)兼容。