FreeMarker 提供了简洁高效的条件运算符,用于处理 null 值、提供默认值和进行条件判断。掌握这些运算符是编写健壮、可维护模板的关键。本指南将深入解析。


核心概念

  1. null 值 (Null Value):表示变量未定义或值为空。直接访问 null 对象的属性或方法会导致 TemplateException
  2. ?? 运算符 (Existence Operator)
    • 功能:检查其左侧的表达式是否“存在”(即不为 null)。
    • 返回值:布尔值 (truefalse)。
    • 用途:常用于 <#if> 条件判断中。
  3. ! 运算符 (Default Value Operator)
    • 功能:如果其左侧的表达式为 null,则返回其右侧指定的默认值;否则返回左侧表达式的值。
    • 返回值:与左侧表达式或右侧默认值同类型。
    • 用途:安全地访问可能为 null 的变量,并提供后备值。
  4. ?? then ... else ... 运算符 (Ternary-like Operator)
    • 功能:这是一个三元运算符的变体。它首先计算 ?? 左侧的表达式。如果该表达式存在(不为 null),则返回 then 后面的表达式;否则返回 else 后面的表达式。
    • 语法expression ?? then_value else else_value
    • 返回值then_valueelse_value 的值。
    • 用途:根据左侧表达式是否存在来选择不同的值或执行不同的逻辑。
  5. 短路求值 (Short-circuit Evaluation)??!?? then ... else ... 都支持短路求值。例如,在 a ?? b 中,如果 a 存在,则不会计算 b;在 a!b 中,如果 a 不为 null,则不会使用 b;在 a ?? x else y 中,如果 a 存在,则不会计算 y

操作步骤 (非常详细)

步骤 1: 使用 ?? 检查变量是否存在

  • 目的:确定一个变量或表达式是否有值(不为 null)。
  • 语法expression ??
  • 详细步骤
    1. 识别潜在的 null 变量:确定模板中哪些数据可能来自外部(如数据库、API),可能为 null。例如:user, user.profile, user.profile.avatarUrl, order.discountCode
    2. <#if> 中使用 ??:这是最常见的用法。
      <#-- 检查 user 对象是否存在 -->
      <#if user??>
        <p>用户: ${user.name}</p>
      <#else>
        <p>用户未登录或信息不可用。</p>
      </#if>
      
    3. 检查嵌套属性:可以链式使用 ?? 检查深层嵌套的对象。
      <#-- 检查 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>
      
    4. 结合逻辑运算符?? 返回布尔值,可以与其他条件组合。
      <#if user?? && user.isActive>
        欢迎回来, ${user.name}!
      </#if>
      

步骤 2: 使用 ! 提供默认值

  • 目的:安全地访问变量,当其为 null 时提供一个替代值。
  • 语法expression!default_valueexpression! (右侧无值时默认为空字符串 "")。
  • 详细步骤
    1. 确定默认值:思考当变量为 null 时,显示什么内容最合适。例如:空字符串 """N/A"0false、一个默认对象或 URL。
    2. 在插值 ${...} 中使用 !:这是最直接的应用场景,避免因 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>
      
    3. assign 中使用 !:将带默认值的结果赋给新变量。
      <#assign displayName = user.nickname!user.username!"Anonymous">
      <!-- 优先使用 nickname,其次 username,最后 "Anonymous" -->
      <p>显示名: ${displayName}</p>
      
    4. 在比较中使用 !:确保比较操作不会因 null 而中断。
      <#-- 安全比较年龄 -->
      <#if (user.age!0) >= 18>
        成年
      <#else>
        未成年
      </#if>
      
    5. 使用 ! 无右侧值:如果 ! 后面没有指定值,则默认为 "" (空字符串)。
      ${user.bio!} <!-- 如果 bio 为 null,输出空字符串 -->
      

步骤 3: 使用 ?? then ... else ... 进行条件选择

  • 目的:根据左侧表达式是否存在,选择执行两个不同分支中的一个。
  • 语法expression ?? then_expression else else_expression
  • 详细步骤
    1. 理解工作原理
      • 首先计算 expression
      • 如果 expression 存在(不为 null),则计算并返回 then_expression 的值。
      • 如果 expression 不存在(为 null),则计算并返回 else_expression 的值。
      • 关键then_expressionelse_expression 是表达式,它们的值会被返回或计算。
    2. 在插值 ${...} 中使用:根据存在性返回不同的字符串或值。
      ${user.email ?? "邮箱: " + user.email else "邮箱未提供"}
      <!-- 如果 user.email 存在,输出 "邮箱: xxx@xx.com";否则输出 "邮箱未提供" -->
      
    3. <#if> 条件中使用:虽然不常见,但可以利用其返回的布尔值或其他值作为条件。
      <#-- 假设 hasPermission 返回布尔值 -->
      <#if user.role ?? hasPermission(user.role, "edit") else false>
        显示编辑按钮
      </#if>
      
    4. assign 中使用:将条件选择的结果赋给变量。
      <#assign avatarUrl = user.profile.avatarUrl ?? user.profile.avatarUrl else "/default-avatar.jpg">
      <!-- 等价于更复杂的 if-else,但更简洁 -->
      <img src="${avatarUrl}" alt="头像">
      
    5. ! 的区别! 只提供一个默认值(当左侧为 null 时使用右侧)。?? then ... else ... 则提供了两个分支:当左侧存在时执行 then 分支,不存在时执行 else 分支。! 更侧重于“补救”,而 ?? then ... else ... 更侧重于“选择”。

步骤 4: 组合使用 ??, !, ?? then ... else ...

  • 目的:处理复杂的 null 场景和条件逻辑。
  • 详细步骤
    1. 链式 !:为嵌套对象提供多级默认值。
      <#assign configValue = appConfig.database.url!"localhost:3306"!"/backup/db">
      <!-- 如果 appConfig 为 null,则第一个 ! 生效,但第二个 ! 无效(因为 "localhost:3306" 不是 null) -->
      <!-- 更好的方式是分步或使用 ?? -->
      
    2. ??! 结合:先用 ?? 判断,再用 ! 提供默认值。
      <#if user.profile?? && user.profile.theme??>
        <#-- theme 存在,但值可能为 null -->
        <#assign theme = user.profile.theme!"light">
      <#else>
        <#assign theme = "light">
      </#if>
      
    3. ?? then ... else ... 包含复杂表达式thenelse 分支可以是复杂的表达式,包括函数调用、其他运算符等。
      ${user.loginCount ?? formatCount(user.loginCount) else "首次登录"}
      <!-- 假设 formatCount 是一个自定义函数 -->
      

步骤 5: 在 Java 代码中传递数据并渲染

  • 目的:确保 Java 层数据与模板中的条件处理逻辑匹配。
  • 详细步骤
    1. 传递可能为 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);
      
    2. 传递布尔标志:有时在 Java 层计算复杂条件,传递布尔值给模板,简化模板逻辑。
      boolean isEligibleForDiscount = user != null && 
                                     user.getAge() >= 60 && 
                                     user.getOrders().size() > 5;
      dataModel.put("isEligibleForDiscount", isEligibleForDiscount);
      
      <#if isEligibleForDiscount>
        您有资格享受老年优惠!
      </#if>
      

常见错误

  1. 混淆 ??!
    • 错误${user.name ?? "Unknown"}?? 返回布尔值,这里试图将其与字符串拼接,通常不是期望的结果。
    • 正确${user.name!"Unknown"} (使用 !) 或 <#if user.name??>${user.name}<#else>Unknown</#if>
  2. ?? 后错误使用 =${user.name ?? "Unknown"} 是错误的,?? 不是赋值。
  3. ?? 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 "未提供"} <!-- 正确,但冗余 -->
      
  4. 过度依赖 ! 而忽略业务逻辑:盲目使用 ! 提供默认值,可能导致隐藏数据缺失的问题,应在必要时进行显式检查(??)并给出明确提示。
  5. 嵌套 ?? 过深:连续使用多个 ?? 检查深层嵌套,代码可读性差。
    • 改进:考虑在 Java 层提供更扁平或更安全的数据结构,或使用 ?has_content (对于集合/字符串)。
  6. ?? then ... else ...then 表达式未被计算:误解了 ?? then ... else ... 的行为。then 后面的表达式只有在左侧存在时才被计算和返回。
  7. ! 后放置可能为 null 的表达式a!(b!c) — 如果 anull,则使用 b!c,但如果 b 也为 null,则最终是 c。逻辑可能复杂,需仔细设计。

注意事项

  1. null 是核心:所有这些运算符都围绕处理 null 值展开。理解数据源中 null 的可能性是正确使用它们的前提。
  2. 短路求值:利用短路求值可以避免不必要的计算或潜在的错误(如访问 null 对象的属性)。
  3. ! 的默认值expression! 等价于 expression!"",即默认为空字符串。对于非字符串类型,可能需要显式指定默认值(如 !0, !false)。
  4. ?? then ... else ...then 关键字非常重要!必须包含 then 关键字。语法是 left ?? then right1 else right2left ?? right1 else right2错误的(在标准 FreeMarker 语法中)。
  5. 性能:这些运算符本身非常轻量级。性能瓶颈通常不在于此,而在于复杂的数据获取或循环。
  6. 可读性:虽然 ?? then ... else ... 很强大,但过于复杂的表达式会降低可读性。适时拆分为 assign 变量或使用传统的 <#if> 结构。
  7. ?default 内建函数的关系?default(value) 内建函数功能上等同于 !value。例如,user.name?default("Unknown") 等价于 user.name!"Unknown"! 语法更简洁。
  8. 作用域:这些运算符不影响变量作用域。

使用技巧

  1. 优先使用 ! 进行安全访问:在 ${} 中输出变量时,养成使用 ! 提供默认值的习惯。
  2. 使用 ?? 进行存在性检查:在需要根据是否存在执行不同逻辑块时(如显示/隐藏元素),使用 ?? 配合 <#if>
  3. ?? then ... else ... 实现简洁的三元选择:当需要根据存在性返回两个不同值时,它是 <#if> 的简洁替代。
    <!-- 简洁 -->
    ${user.avatar ?? user.avatar else "/default.png"}
    <!-- 等价但 verbose -->
    <#if user.avatar??>
      ${user.avatar}
    <#else>
      /default.png
    </#if>
    
  4. 结合内建函数
    • ?has_content:对于字符串和集合,比 ?? 更语义化(检查非空)。
      <#if user.comments?has_content>
        <!-- 显示评论列表 -->
      </#if>
      
    • ?if_exists:仅当变量存在时才输出其值,否则输出空字符串。${user.name?if_exists} 等价于 ${user.name!}
  5. 在宏中使用:将常见的 null 处理模式封装成宏。
    <#macro safeDisplay value defaultMsg="N/A">
      ${value!defaultMsg}
    </#macro>
    <@safeDisplay value=user.email defaultMsg="未填写邮箱"/>
    

最佳实践与性能优化

  1. 防御性编程:始终假设传入模板的数据可能包含 null 值。在访问任何属性前,使用 ??! 进行防护。
  2. 明确意图
    • 使用 ! 当您想提供一个后备值
    • 使用 ?? 当您想根据存在性逻辑分支
    • 使用 ?? then ... else ... 当您想根据存在性****选择返回两个不同值
  3. 简化模板逻辑:复杂的业务规则和条件判断应尽量在 Java 代码中完成,传递清晰、预处理过的数据(如布尔标志、计算好的字符串)到模板。模板应专注于展示。
  4. 避免过度嵌套:减少 ??<#if> 的嵌套层级,提高可读性。可以提取中间变量 (<#assign>)。
  5. 利用 ?has_content:对于集合和字符串的“非空”检查,优先使用 ?has_content,语义更清晰。
  6. 重用 Configuration,缓存 Template:这是 FreeMarker 性能优化的基石,与运算符无关但至关重要。
  7. 保持表达式简洁:避免在单个 ${} 中构建过长或过于复杂的 ?? then ... else ... 表达式。拆分有助于维护。
  8. 测试 null 场景:编写测试用例,确保当关键数据为 null 时,模板能正确降级显示(如显示默认图片、提示信息)。
  9. 文档化默认值:如果使用了重要的默认值,可以在模板注释中说明,便于维护。
  10. 版本一致性:确认团队使用的 FreeMarker 版本,确保语法(特别是 ?? then ... else ...)兼容。