FreeMarker 逻辑运算符用于在模板中进行条件判断、布尔值组合等操作,是控制模板输出流程的核心工具。本文将系统介绍 FreeMarker 的逻辑运算符,涵盖核心概念、详细操作步骤、常见错误、注意事项、使用技巧以及最佳实践与性能优化。


1. 核心概念

逻辑运算符用于操作布尔值(truefalse),通常用于 <#if>, <#elseif>, <#else> 等指令的条件表达式中,以决定模板的哪一部分应该被渲染。

主要逻辑运算符

运算符 描述 示例
&& (或 and) 逻辑与 (AND) ${a && b}${a and b}
|| (或 or) 逻辑或 (OR) ${a || b}${a or b}
! (或 not) 逻辑非 (NOT) ${!a}${not a}
== 等于 (比较值) ${a == b}
!= 不等于 ${a != b}
> 大于 ${a > b}
>= 大于等于 ${a >= b}
< 小于 ${a < b}
<= 小于等于 ${a <= b}

比较运算符与逻辑运算符的关系

  • 比较运算符(如 ==, !=, >, < 等)用于比较两个值,其结果是一个布尔值。
  • 逻辑运算符(如 &&, ||, !)用于组合或修饰布尔值。
  • 它们通常结合使用,例如:${(age >= 18) && (hasLicense == true)}

布尔值来源

  • 数据模型中直接传递的布尔值(true/false)。
  • 比较表达式的结果。
  • 内建函数的结果(如 ?has_content, ?exists)。
  • 空值检查(??)。

2. 操作步骤(非常详细)

以下是使用 FreeMarker 逻辑运算符的详细步骤,帮助您快速掌握并实践。

步骤 1:准备开发环境

  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'
      
  2. 创建模板文件

    • src/main/resources/templates 目录下创建 logic_operators.ftl

步骤 2:定义数据模型(Java 代码)

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

import java.io.*;
import java.util.HashMap;
import java.util.Map;

public class LogicOperatorsExample {
    public static void main(String[] args) throws IOException, TemplateException {
        // 1. 创建 FreeMarker 配置
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
        cfg.setClassForTemplateLoading(LogicOperatorsExample.class, "/templates");
        
        // 2. 获取模板
        Template template = cfg.getTemplate("logic_operators.ftl");
        
        // 3. 创建数据模型
        Map<String, Object> dataModel = new HashMap<>();
        
        // 用户信息
        dataModel.put("username", "Alice");
        dataModel.put("age", 25);
        dataModel.put("isPremium", true);
        dataModel.put("isLoggedIn", true);
        dataModel.put("accountStatus", "ACTIVE"); // ACTIVE, INACTIVE, SUSPENDED
        
        // 购物车信息
        dataModel.put("cartTotal", 150.0);
        dataModel.put("hasCoupon", true);
        dataModel.put("couponCode", "SAVE10");
        dataModel.put("minimumSpend", 100.0);
        
        // 列表数据
        dataModel.put("items", new String[]{"Laptop", "Mouse", "Keyboard"});
        
        // 4. 准备输出
        Writer out = new OutputStreamWriter(System.out);
        
        // 5. 合并模板与数据
        template.process(dataModel, out);
        
        // 6. 清理
        out.flush();
        out.close();
    }
}

步骤 3:编写模板文件(logic_operators.ftl

<#-- 逻辑运算符示例 -->
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>FreeMarker 逻辑运算符演示</title>
</head>
<body>

<h1>用户信息</h1>

<#-- 1. 逻辑与 (&& / and) -->
<#-- 检查是否为成年且登录的用户 -->
<#if (age >= 18) && isLoggedIn>
    <p>欢迎, ${username}! 您是成年登录用户。</p>
<#else>
    <p>欢迎访问!</p>
</#if>

<#-- 2. 逻辑或 (|| / or) -->
<#-- 检查账户状态是否正常(ACTIVE 或 INACTIVE) -->
<#if (accountStatus == "ACTIVE") || (accountStatus == "INACTIVE")>
    <p>您的账户状态正常。</p>
<#else>
    <p>警告:您的账户可能已被暂停或存在异常。</p>
</#if>

<#-- 3. 逻辑非 (! / not) -->
<#-- 检查是否不是高级会员 -->
<#if !isPremium>
    <p>升级为高级会员可享受更多权益!</p>
<#else>
    <p>感谢您成为高级会员!</p>
</#if>

<#-- 4. 复合条件 (结合 &&, ||, !) -->
<#-- 检查是否有资格使用优惠券:购物车总额 >= 最低消费 且 有优惠券 且 账户状态正常 -->
<#assign eligibleForCoupon = 
    (cartTotal >= minimumSpend) 
    && hasCoupon 
    && (accountStatus == "ACTIVE")>
    
<#if eligibleForCoupon>
    <p>恭喜!您有资格使用优惠券 "${couponCode}"。</p>
<#else>
    <p>您暂时无法使用优惠券。</p>
    <#-- 使用 ! 和 || 分析原因 -->
    <#if !(cartTotal >= minimumSpend)>
        <p>- 原因:购物车金额需达到 ${minimumSpend?string.currency} 才能使用。</p>
    <#elseif !hasCoupon>
        <p>- 原因:您没有可用的优惠券。</p>
    <#elseif (accountStatus != "ACTIVE")>
        <p>- 原因:您的账户状态异常。</p>
    </#if>
</#if>

<#-- 5. 空值检查 (??) 与 逻辑结合 -->
<#-- 安全地访问可能为 null 的属性 -->
<#if (profile?.address??) && (profile?.address != "")>
    <p>您的地址: ${profile.address}</p>
<#else>
    <p>请完善您的地址信息。</p>
</#if>
<#-- 注意:上面假设 profile 可能为 null,profile.address 也可能为 null 或空字符串 -->
<#-- 在本例数据模型中 profile 未定义,所以会显示 "请完善..." -->

<#-- 6. 使用内建函数进行条件判断 -->
<#-- ?has_content 检查字符串或集合是否非空且非仅空白 -->
<#if items?has_content>
    <h2>购物车商品 (${items?size} 件):</h2>
    <ul>
    <#list items as item>
        <li>${item_index + 1}. ${item}</li>
    </#list>
    </ul>
<#else>
    <p>您的购物车为空。</p>
</#if>

<#-- 7. ?exists 检查变量是否存在 -->
<#if userSettings?exists>
    <p>用户设置已加载。</p>
<#else>
    <p>用户设置未提供。</p>
    <#-- 通常与 ?? 配合使用提供默认值 -->
    <#assign theme = "light">
</#if>

<#-- 8. 字符串比较 (注意大小写) -->
<#-- 使用 ?lower_case 或 ?upper_case 进行不区分大小写的比较 -->
<#if (accountStatus?lower_case == "active")>
    <p>账户状态:激活中</p>
<#else>
    <p>账户状态:${accountStatus}</p>
</#if>

<#-- 9. 数字比较 -->
<#if cartTotal > 200>
    <p>您即将达到 VIP 消费等级!</p>
<#elseif cartTotal > 100>
    <p>消费可观,继续加油!</p>
<#else>
    <p>继续购物吧!</p>
</#if>

<#-- 10. 组合多个条件 (复杂示例) -->
<#-- 判断是否显示“快速结账”按钮:登录、购物车有内容、账户状态正常、不是新用户(年龄>18) -->
<#assign showExpressCheckout = 
    isLoggedIn 
    && (items?has_content) 
    && (accountStatus == "ACTIVE") 
    && (age > 18)>
    
<#if showExpressCheckout>
    <button>快速结账</button>
<#else>
    <button>去结算</button>
</#if>

</body>
</html>

步骤 4:运行程序并查看结果

  1. 编译并运行 LogicOperatorsExample.java
  2. 查看控制台输出的 HTML 内容,验证逻辑判断是否按预期工作。

3. 常见错误

  1. 使用 = 而不是 ==

    • 错误${a = b}
    • 原因= 是赋值运算符(在 <#assign> 中使用),比较相等应使用 ==
    • 解决:始终使用 == 进行相等性比较。
  2. 混淆 &&||

    • 错误:本意是“登录且是高级会员”,却写成 ${isLoggedIn || isPremium}
    • 解决:仔细检查逻辑需求,使用正确的运算符。&& 要求所有条件为真,|| 要求至少一个条件为真。
  3. 运算符优先级问题

    • 问题${a || b && c} 的执行顺序是 a || (b && c),因为 && 优先级高于 ||。如果本意是 (a || b) && c,则结果错误。
    • 解决始终使用括号 () 明确指定运算顺序,避免依赖默认优先级。
  4. 空值 (null) 导致错误

    • 错误${user.profile.address == "Beijing"},如果 userprofilenull,会抛出异常。
    • 解决:使用安全导航操作符 ???
      • ${(user?.profile?.address??) == "Beijing"} (先检查 null,再比较)
      • 或者先检查存在性:<#if user?exists && user.profile?exists && user.profile.address == "Beijing">
  5. 字符串比较区分大小写

    • 问题${status == "active"} 无法匹配 status="ACTIVE"
    • 解决:使用 ?lower_case?upper_case 进行标准化比较:${status?lower_case == "active"}
  6. 在条件中直接使用可能为 null 的数字或字符串进行比较

    • 错误${age > 18},如果 agenull
    • 解决${(age??0) > 18}<#if age??>${age > 18}<#else>false</#if>

4. 注意事项

  1. 短路求值 (Short-circuit evaluation)

    • FreeMarker 支持短路求值。对于 &&,如果左操作数为 false,则不会计算右操作数。对于 ||,如果左操作数为 true,则不会计算右操作数。
    • 注意:这意味着右操作数中的表达式(可能包含函数调用或副作用)可能不会被执行。虽然 FreeMarker 模板通常避免副作用,但需知晓此行为。
  2. ?has_content 的行为

    • 对于字符串:检查是否非 null 且长度 > 0 且不全是空白字符。
    • 对于集合(List, Map):检查是否非 null?size > 0
    • 对于数字、布尔值:总是返回 true(因为它们存在且有值)。
    • 对于日期:总是返回 true
    • 注意?has_content 是检查“是否有实际内容”的常用方法,比 ?? 更严格(?? 只检查 null)。
  3. ?exists vs ??

    • ?exists:检查变量是否在当前作用域中被定义(即使其值为 null,也返回 true)。
    • ??:检查变量是否非 null
    • 区别:如果一个变量被明确设置为 nullvariable?existstrue,但 variable??false
  4. 避免在条件中进行复杂计算

    • 虽然可以在 <#if> 中进行计算,但复杂的表达式会降低可读性。优先使用 <#assign> 将计算结果赋给一个变量,然后在条件中使用该变量。
  5. ! 运算符的歧义

    • ! 既可以是逻辑非 !a,也可以是默认值操作符 a!default
    • 注意:在 ! 后紧跟变量名时是默认值操作符。要表示逻辑非,通常需要括号或确保上下文清晰,例如 !(a == b)!condition。避免写 !a == b,应写 !(a == b)

5. 使用技巧

  1. 使用 <#assign> 提高可读性

    <#assign isEligible = (user.age >= 18) && user.verified && (user.balance > 100)>
    <#if isEligible>
        ...
    </#if>
    

    比直接在 <#if> 中写长表达式更清晰。

  2. 利用 ?has_content 简化空检查

    <#-- 好 -->
    <#if items?has_content>
        <#list items as item>...</#list>
    </#if>
    
    <#-- 避免(不够简洁) -->
    <#if items?? && items?size > 0>
        <#list items as item>...</#list>
    </#if>
    
  3. 组合 ??! 提供默认值

    <#-- 如果 user.name 为 null,则使用 "访客" -->
    <p>欢迎, ${user.name! "访客"}!</p>
    <#-- 注意 ! 后的空格是可选的,但推荐加上空格提高可读性 -->
    
  4. 使用宏(Macro)封装复杂逻辑

    <#macro checkUserEligibility user>
        <#return (user.age >= 18) && user.active && (user.score > 50)>
    </#macro>
    
    <#-- 使用 -->
    <#if checkUserEligibility(currentUser)>
        <p>您符合资格。</p>
    </#if>
    
  5. 利用 ?then 内建函数进行条件选择

    • ${condition?then("是", "否")}:如果 condition 为真,返回第一个参数,否则返回第二个。
    • 这可以替代简单的 <#if> 结构,使代码更紧凑。
    • 示例:<p>状态: ${isActive?then("激活", "未激活")}</p>

6. 最佳实践与性能优化

  1. 逻辑前置(核心原则)

    • 最佳实践:将复杂的业务规则、权限判断、资格验证等逻辑放在 Java 服务层完成。FreeMarker 模板应主要处理展示逻辑和简单的条件渲染。
    • 优点:后端逻辑更易于单元测试、调试和维护;模板更简洁、易读;性能通常更好(JVM 执行 Java 代码比模板引擎解析表达式快)。
  2. 缓存与配置

    • 确保 Configuration 实例被正确配置和重用(通常是单例)。
    • 利用 FreeMarker 的模板缓存机制(默认开启),避免重复解析模板文件。
    • 设置合理的 template_update_delay(模板更新检查间隔)。
  3. 减少模板中的计算量

    • 避免在循环(#list)内部进行昂贵的计算或数据库查询(即使通过宏调用)。
    • 将循环中不变的计算提取到循环外部。
  4. 使用 ?exists 谨慎

    • 频繁使用 ?exists 可能影响性能,因为它需要在作用域链中查找变量。如果变量预期存在,直接使用通常更快。?exists 主要用于处理可选变量。
  5. 避免深层嵌套的 <#if>

    • 过多的嵌套 <#if> 会使模板难以阅读和维护。
    • 优化
      • 使用 <#if> <#elseif> <#else> 结构替代多个独立的 <#if>
      • 提前返回或使用 ! 操作符处理否定情况。
      • 将复杂的分支逻辑拆分成多个宏或子模板。
  6. 性能监控与分析

    • 在生产环境中,监控模板渲染时间。
    • 如果发现特定模板或条件判断是瓶颈,优先考虑将其逻辑移至后端。
    • 使用性能分析工具定位耗时操作。

通过遵循以上指南,您将能够高效、正确地使用 FreeMarker 逻辑运算符,构建健壮且高性能的模板系统。记住,清晰、可维护的模板代码往往比追求极致性能的复杂表达式更重要。