FreeMarker 内建函数(Built-ins)是 FreeMarker 模板语言中一组强大的、预定义的函数,它们可以直接作用于变量,用于格式化、转换、查询或操作数据。熟练掌握内建函数是编写高效、清晰、功能丰富的 FreeMarker 模板的关键。本文将系统介绍 FreeMarker 的内建函数,涵盖核心概念、详细操作步骤、常见错误、注意事项、使用技巧以及最佳实践与性能优化。


1. 核心概念

什么是内建函数 (Built-ins)

  • 定义:内建函数是 FreeMarker 提供的、可以直接附加在变量(或表达式)后面的特殊方法,用于执行特定操作。语法为 variable?function_namevariable?function_name(parameters)
  • 目的:简化模板中的数据处理逻辑,如格式化日期、数字,检查变量状态,转换数据类型,操作序列和哈希表等。
  • 分类:内建函数根据其作用的对象类型(字符串、数字、日期、序列、哈希表、布尔值等)和功能进行分类。

主要内建函数类别

  1. 通用内建函数:适用于多种类型。
    • ?exists:检查变量是否在当前作用域中被定义。
    • ??:检查变量是否非 null
    • ?string:将任何值转换为字符串(有多种格式化选项)。
  2. 字符串内建函数
    • ?upper_case, ?lower_case:大小写转换。
    • ?trim:去除首尾空白。
    • ?length:获取字符串长度。
    • ?substring, ?split:子串和分割。
    • ?replace:替换字符。
  3. 数字内建函数
    • ?string:格式化数字(?string("0.##"), ?string.currency, ?string.percent)。
    • ?int, ?c:转换为整数或计算机可读格式。
  4. 日期/时间内建函数
    • ?string:格式化日期(?string("yyyy-MM-dd"), ?string.short, ?string.medium)。
    • ?date, ?time, ?datetime:提取日期、时间或日期时间部分。
  5. 序列内建函数
    • ?size:获取序列长度。
    • ?first, ?last:获取首尾元素。
    • ?sort, ?sort_by:排序。
    • ?join:连接元素。
    • ?map, ?filter:映射和过滤。
    • ?chunk:分块。
  6. 哈希表内建函数
    • ?keys, ?values:获取键或值的序列。
    • ?size:获取键值对数量。
  7. 布尔值内建函数
    • ?string:将布尔值转换为字符串(?string("yes", "no"))。

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 目录下创建 builtins_demo.ftl

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

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

import java.io.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;

public class BuiltinsDemo {
    public static void main(String[] args) throws IOException, TemplateException {
        // 1. 创建 FreeMarker 配置
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
        cfg.setClassForTemplateLoading(BuiltinsDemo.class, "/templates");
        
        // 2. 获取模板
        Template template = cfg.getTemplate("builtins_demo.ftl");
        
        // 3. 创建数据模型
        Map<String, Object> dataModel = new HashMap<>();
        
        // 字符串数据
        dataModel.put("title", "  Welcome to FreeMarker Tutorial!  ");
        dataModel.put("description", "This is a comprehensive guide.");
        dataModel.put("status", "ACTIVE");
        dataModel.put("email", "user@example.com");
        dataModel.put("phone", "123-456-7890");
        
        // 数字数据
        dataModel.put("price", new BigDecimal("129.99"));
        dataModel.put("discount", 0.15);
        dataModel.put("quantity", 5);
        dataModel.put("rating", 4.7);
        dataModel.put("largeNumber", 1000000L);
        
        // 日期/时间数据
        dataModel.put("today", LocalDate.now());
        dataModel.put("now", LocalDateTime.now());
        dataModel.put("eventDate", LocalDate.of(2025, 12, 25));
        dataModel.put("signupTime", LocalDateTime.of(2025, 3, 15, 10, 30));
        
        // 序列数据
        List<String> tags = Arrays.asList("tutorial", "freemarker", "template", "java");
        dataModel.put("tags", tags);
        
        List<Map<String, Object>> products = new ArrayList<>();
        Map<String, Object> prod1 = new HashMap<>();
        prod1.put("name", "Laptop");
        prod1.put("price", new BigDecimal("999.99"));
        prod1.put("category", "Electronics");
        prod1.put("inStock", true);
        products.add(prod1);
        
        Map<String, Object> prod2 = new HashMap<>();
        prod2.put("name", "Coffee Mug");
        prod2.put("price", new BigDecimal("15.50"));
        prod2.put("category", "Home");
        prod2.put("inStock", false);
        products.add(prod2);
        
        dataModel.put("products", products);
        
        // 哈希表数据
        Map<String, String> userInfo = new HashMap<>();
        userInfo.put("name", "Alice");
        userInfo.put("role", "Developer");
        userInfo.put("department", "Engineering");
        dataModel.put("userInfo", userInfo);
        
        // 布尔值数据
        dataModel.put("isActive", true);
        dataModel.put("hasPremium", false);
        
        // 可能为 null 的数据
        dataModel.put("optionalField", null);
        dataModel.put("emptyList", Collections.emptyList());
        
        // 4. 准备输出
        Writer out = new OutputStreamWriter(System.out);
        
        // 5. 合并模板与数据
        template.process(dataModel, out);
        
        // 6. 清理
        out.flush();
        out.close();
    }
}

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

<#-- 内建函数演示 -->
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>FreeMarker 内建函数演示</title>
    <style>
        .section { margin-bottom: 20px; }
        .code { background: #f4f4f4; padding: 5px; font-family: monospace; }
        .highlight { background: yellow; }
    </style>
</head>
<body>

<h1>FreeMarker 内建函数 (Built-ins) 演示</h1>

<div class="section">
    <h2>1. 通用内建函数</h2>
    
    <#-- ?exists: 检查变量是否定义 -->
    <p>?exists: <code>optionalField?exists</code> = ${optionalField?exists?string("true", "false")}</p>
    <p>?exists: <code>nonExistentVar?exists</code> = ${nonExistentVar?exists?string("true", "false", "undefined")}</p>
    
    <#-- ?? (非 null 检查) -->
    <p>??: <code>optionalField??</code> = ${optionalField???"null"}</p>
    <p>??: <code>title??</code> = ${title???"null"}</p>
    
    <#-- ?string: 通用字符串转换 -->
    <p>?string: <code>isActive?string</code> = ${isActive?string}</p>
    <p>?string: <code>quantity?string</code> = ${quantity?string}</p>
    <p>?string: <code>today?string</code> = ${today?string}</p>
</div>

<div class="section">
    <h2>2. 字符串内建函数</h2>
    
    <#-- ?upper_case / ?lower_case -->
    <p>?upper_case: <code>title?upper_case</code> = <span class="highlight">${title?upper_case}</span></p>
    <p>?lower_case: <code>status?lower_case</code> = ${status?lower_case}</p>
    
    <#-- ?trim -->
    <p>?trim: <code>title?trim</code> = <span class="highlight">"${title?trim}"</span> (去除了首尾空格)</p>
    
    <#-- ?length -->
    <p>?length: <code>description?length</code> = ${description?length}</p>
    
    <#-- ?substring -->
    <p>?substring: <code>description?substring(0, 10)</code> = ${description?substring(0, 10)}...</p>
    
    <#-- ?split -->
    <p>?split: <code>phone?split("-")</code> = 
        <#list phone?split("-") as part>${part}<#sep> - </#list>
    </p>
    
    <#-- ?replace -->
    <p>?replace: <code>email?replace("@", " [at] ")</code> = ${email?replace("@", " [at] ")}</p>
    <p>?replace: <code>description?replace(" ", "_")</code> = ${description?replace(" ", "_")}</p>
    
    <#-- ?cap_first / ?uncap_first -->
    <p>?cap_first: <code>title?trim?cap_first</code> = ${title?trim?cap_first}</p>
    <p>?uncap_first: <code>description?uncap_first</code> = ${description?uncap_first}</p>
</div>

<div class="section">
    <h2>3. 数字内建函数</h2>
    
    <#-- ?string 格式化数字 -->
    <p>?string.currency: <code>price?string.currency</code> = ${price?string.currency}</p>
    <p>?string.percent: <code>discount?string.percent</code> = ${discount?string.percent}</p>
    <p>?string("0.00"): <code>rating?string("0.00")</code> = ${rating?string("0.00")}</p>
    <p>?string("0.##"): <code>rating?string("0.##")</code> = ${rating?string("0.##")}</p>
    <p>?string("0,000"): <code>largeNumber?string("0,000")</code> = ${largeNumber?string("0,000")}</p>
    
    <#-- ?int -->
    <p>?int: <code>price?int</code> = ${price?int} (向下取整)</p>
    <p>?int: <code>rating?int</code> = ${rating?int}</p>
    
    <#-- ?c (计算机格式) -->
    <p>?c: <code>price?c</code> = ${price?c} (用于 JavaScript/JSON)</p>
</div>

<div class="section">
    <h2>4. 日期/时间内建函数</h2>
    
    <#-- ?string 格式化日期 -->
    <p>?string("yyyy-MM-dd"): <code>today?string("yyyy-MM-dd")</code> = ${today?string("yyyy-MM-dd")}</p>
    <p>?string("dd/MM/yyyy"): <code>eventDate?string("dd/MM/yyyy")</code> = ${eventDate?string("dd/MM/yyyy")}</p>
    <p>?string("HH:mm:ss"): <code>now?string("HH:mm:ss")</code> = ${now?string("HH:mm:ss")}</p>
    <p>?string("yyyy-MM-dd HH:mm"): <code>signupTime?string("yyyy-MM-dd HH:mm")</code> = ${signupTime?string("yyyy-MM-dd HH:mm")}</p>
    
    <#-- ?string.short / ?string.medium / ?string.long -->
    <p>?string.short: <code>today?string.short</code> = ${today?string.short}</p>
    <p>?string.medium: <code>today?string.medium</code> = ${today?string.medium}</p>
    
    <#-- ?date, ?time, ?datetime -->
    <p>?date: <code>signupTime?date</code> = ${signupTime?date}</p>
    <p>?time: <code>signupTime?time</code> = ${signupTime?time}</p>
    <p>?datetime: <code>signupTime?datetime</code> = ${signupTime?datetime}</p>
</div>

<div class="section">
    <h2>5. 序列内建函数</h2>
    
    <#-- ?size -->
    <p>?size: <code>tags?size</code> = ${tags?size}</p>
    <p>?size: <code>products?size</code> = ${products?size}</p>
    <p>?size: <code>emptyList?size</code> = ${emptyList?size}</p>
    
    <#-- ?first / ?last -->
    <#if tags?has_content>
        <p>?first: <code>tags?first</code> = ${tags?first}</p>
        <p>?last: <code>tags?last</code> = ${tags?last}</p>
    </#if>
    
    <#-- ?join -->
    <p>?join: <code>tags?join(", ")</code> = ${tags?join(", ")}</p>
    <p>?join: <code>tags?join(" | ")</code> = ${tags?join(" | ")}</p>
    
    <#-- ?sort / ?sort_by -->
    <p>?sort: <code>tags?sort?join(", ")</code> = ${tags?sort?join(", ")}</p>
    <p>?sort_by("price"): 
        <#list products?sort_by("price") as prod>
            ${prod.name}(${prod.price?string.currency})<#sep>, 
        </#list>
    </p>
    <p>?sort_by("-price"): 
        <#list products?sort_by("-price") as prod>
            ${prod.name}(${prod.price?string.currency})<#sep>, 
        </#list>
    </p>
    
    <#-- ?map / ?filter -->
    <p>?map: 产品名称 <code>products?map(p -> p.name)?join(", ")</code> = 
        ${products?map(p -> p.name)?join(", ")}
    </p>
    <p>?filter: 库存产品 <code>products?filter(p -> p.inStock)?map(p -> p.name)?join(", ")</code> = 
        ${products?filter(p -> p.inStock)?map(p -> p.name)?join(", ")}
    </p>
    
    <#-- ?chunk -->
    <p>?chunk(2): 
        <#list products?chunk(2) as productRow>
            <div style="border:1px solid #ccc; margin:5px; padding:5px;">
                <#list productRow as prod>
                    ${prod.name} 
                </#list>
            </div>
        </#list>
    </p>
</div>

<div class="section">
    <h2>6. 哈希表内建函数</h2>
    
    <#-- ?keys / ?values -->
    <p>?keys: <code>userInfo?keys?join(", ")</code> = ${userInfo?keys?join(", ")}</p>
    <p>?values: <code>userInfo?values?join(", ")</code> = ${userInfo?values?join(", ")}</p>
    
    <#-- ?size -->
    <p>?size: <code>userInfo?size</code> = ${userInfo?size}</p>
</div>

<div class="section">
    <h2>7. 布尔值内建函数</h2>
    
    <#-- ?string -->
    <p>?string("Yes", "No"): <code>isActive?string("Yes", "No")</code> = ${isActive?string("Yes", "No")}</p>
    <p>?string("Enabled", "Disabled"): <code>hasPremium?string("Enabled", "Disabled")</code> = ${hasPremium?string("Enabled", "Disabled")}</p>
    
    <#-- ?then -->
    <p>?then: <code>isActive?then("Active", "Inactive")</code> = ${isActive?then("Active", "Inactive")}</p>
    <p>?then: <code>hasPremium?then("Premium User", "Regular User")</code> = ${hasPremium?then("Premium User", "Regular User")}</p>
</div>

<div class="section">
    <h2>8. 组合使用内建函数</h2>
    
    <#-- 链式调用 -->
    <p>链式调用: <code>title?trim?upper_case?length</code> = ${title?trim?upper_case?length}</p>
    <p>链式调用: <code>tags?sort?join(" · ")</code> = ${tags?sort?join(" · ")}</p>
    
    <#-- 与条件结合 -->
    <#if (price * (1 - discount))?c gt 100>
        <p>折扣后价格大于 100: ${(price * (1 - discount))?string.currency}</p>
    </#if>
</div>

</body>
</html>

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

  1. 编译并运行 BuiltinsDemo.java
  2. 查看控制台输出的 HTML 内容,验证各种内建函数的应用效果。

3. 常见错误

  1. 在错误类型的变量上调用内建函数

    • 错误${123?upper_case}
    • 原因?upper_case 是字符串内建函数,不能用于数字。
    • 解决:确保内建函数应用于正确的数据类型。必要时先用 ?string 转换:${(123)?string?upper_case}
  2. 忽略 null 值导致异常

    • 错误${optionalField?length}${optionalField?first}
    • 原因optionalFieldnull,调用其内建函数会抛出异常。
    • 解决:使用 ?? 提供默认值或先用 ??/?exists 检查。
      • ${(optionalField!"")?length}
      • <#if optionalField??>${optionalField?length}</#if>
  3. 混淆 ?string 的不同用法

    • 问题${date?string} vs ${date?string("yyyy-MM-dd")}
    • 解决:明确 ?string 的参数。无参数时是通用转换,有参数时是特定格式化(日期、数字)。
  4. ?map?filter 的 Lambda 语法错误

    • 错误products?map(p -> p.name (缺少右括号) 或 products?filter(p -> p.price > 100 (缺少右括号)。
    • 解决:注意语法 sequence?function(item -> expression),确保括号匹配。
  5. 对空序列使用 ?first/?last

    • 错误${emptyList?first}
    • 原因:空序列没有首尾元素。
    • 解决:先用 ?has_content 检查:<#if list?has_content>${list?first}</#if>
  6. ?sort 修改原序列的误解

    • 问题:认为 ?sort 会改变原始序列。
    • 解决?sort 返回新序列,原序列不变。需要时用 <#assign> 接收:<#assign sortedTags = tags?sort>

4. 注意事项

  1. ?has_content 的妙用

    • 对于字符串和序列,?has_content 是检查“非空”的最佳选择,它等价于 ?? && ?length > 0(字符串)或 ?? && ?size > 0(序列)。
  2. ?string 的多面性

    • ?string 本身是通用转换。
    • ?string("format") 用于日期/数字格式化。
    • ?string("trueText", "falseText") 用于布尔值。
    • 上下文决定其行为。
  3. ?c 的用途

    • ?c 将数字、布尔值、日期等转换为计算机可读的、无本地化格式的字符串,非常适合在 JavaScript 或 JSON 中使用。
  4. ?exists vs ??

    • variable?exists: 变量在作用域中被定义(即使值为 null,也返回 true)。
    • variable??: 变量非 null
    • 区别在于 null 值的处理。
  5. 链式调用 (Chaining)

    • 内建函数可以链式调用:variable?func1?func2?func3
    • 执行顺序从左到右:先 func1,结果传给 func2,依此类推。
  6. 性能

    • 大多数内建函数执行很快。
    • ?sort, ?filter, ?map 对大型序列可能较慢。
    • ?join 对大型序列生成大字符串可能消耗内存。

5. 使用技巧

  1. 安全访问链

    <#-- 安全地访问嵌套属性 -->
    <#-- 方法1: 多层检查 -->
    <#if (user??) && (user.profile??) && (user.profile.address??)>
        ${user.profile.address}
    </#if>
    
    <#-- 方法2: 使用 ?default (FreeMarker 2.3.22+) 或 ?? 配合默认值 -->
    <#-- 假设 user.profile.address 可能为 null -->
    ${((user.profile.address)!"Unknown")?trim}
    
  2. ?then 内建函数

    • ${condition?then("value1", "value2")}<#if condition>value1<#else>value2</#if> 的简洁替代,适合简单条件选择。
  3. ?chunk 用于布局

    • 非常适合将列表数据按行(如每行 3 个)或分组展示,简化 HTML 表格或网格布局。
  4. 利用 ?keys/?values 遍历哈希表

    <#list userInfo?keys as key>
        <p><strong>${key?cap_first}:</strong> ${userInfo[key]}</p>
    </#list>
    
  5. ?replace 的正则支持

    • ?replace(pattern, replacement, "r") 支持正则表达式替换("r" 标志)。

6. 最佳实践与性能优化

  1. 数据预处理优先

    • 最佳实践:在 Java 后端完成复杂的转换、过滤、排序、聚合计算。将处理好的、适合展示的数据传递给模板。
    • 优点:模板更简单、更易维护;后端逻辑更易测试;通常性能更好(JVM 优化)。
  2. 避免在模板中处理大数据集

    • 不要在模板中对包含成千上万条记录的序列进行 ?sort?filter 等操作。
    • 优化:在数据库或服务层完成分页、过滤和排序,模板只负责渲染当前页的数据。
  3. 合理使用缓存

    • 配置并利用 FreeMarker 的模板缓存。
    • 对于计算成本高且结果稳定的内建函数调用(如复杂 ?map/?filter),考虑在 Java 层缓存结果。
  4. 减少嵌套和复杂表达式

    • 避免过长的内建函数链或嵌套条件。
    • 优化:使用 <#assign> 将中间结果存储在变量中,提高可读性。
  5. 使用 ?c 保证数据一致性

    • 当需要将数字、布尔值嵌入 JavaScript 或 JSON 时,务必使用 ?c,避免因本地化格式(如千位分隔符)导致解析错误。
  6. 监控与分析

    • 关注模板渲染性能,特别是包含大量内建函数操作的模板。
    • 使用性能分析工具定位瓶颈,优先考虑将耗时操作移至后端。

掌握 FreeMarker 内建函数是提升模板开发效率和质量的关键。遵循最佳实践,合理运用这些强大的工具,可以创建出既功能强大又易于维护的动态内容。