FreeMarker 内建函数(Built-ins)是 FreeMarker 模板语言中一组强大的、预定义的函数,它们可以直接作用于变量,用于格式化、转换、查询或操作数据。熟练掌握内建函数是编写高效、清晰、功能丰富的 FreeMarker 模板的关键。本文将系统介绍 FreeMarker 的内建函数,涵盖核心概念、详细操作步骤、常见错误、注意事项、使用技巧以及最佳实践与性能优化。
1. 核心概念
什么是内建函数 (Built-ins)
- 定义:内建函数是 FreeMarker 提供的、可以直接附加在变量(或表达式)后面的特殊方法,用于执行特定操作。语法为
variable?function_name
或variable?function_name(parameters)
。 - 目的:简化模板中的数据处理逻辑,如格式化日期、数字,检查变量状态,转换数据类型,操作序列和哈希表等。
- 分类:内建函数根据其作用的对象类型(字符串、数字、日期、序列、哈希表、布尔值等)和功能进行分类。
主要内建函数类别
- 通用内建函数:适用于多种类型。
?exists
:检查变量是否在当前作用域中被定义。??
:检查变量是否非null
。?string
:将任何值转换为字符串(有多种格式化选项)。
- 字符串内建函数:
?upper_case
,?lower_case
:大小写转换。?trim
:去除首尾空白。?length
:获取字符串长度。?substring
,?split
:子串和分割。?replace
:替换字符。
- 数字内建函数:
?string
:格式化数字(?string("0.##")
,?string.currency
,?string.percent
)。?int
,?c
:转换为整数或计算机可读格式。
- 日期/时间内建函数:
?string
:格式化日期(?string("yyyy-MM-dd")
,?string.short
,?string.medium
)。?date
,?time
,?datetime
:提取日期、时间或日期时间部分。
- 序列内建函数:
?size
:获取序列长度。?first
,?last
:获取首尾元素。?sort
,?sort_by
:排序。?join
:连接元素。?map
,?filter
:映射和过滤。?chunk
:分块。
- 哈希表内建函数:
?keys
,?values
:获取键或值的序列。?size
:获取键值对数量。
- 布尔值内建函数:
?string
:将布尔值转换为字符串(?string("yes", "no")
)。
2. 操作步骤(非常详细)
以下是使用 FreeMarker 内建函数的详细步骤,帮助您快速掌握并实践。
步骤 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'
- Maven (
创建模板文件:
- 在
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:运行程序并查看结果
- 编译并运行
BuiltinsDemo.java
。 - 查看控制台输出的 HTML 内容,验证各种内建函数的应用效果。
3. 常见错误
在错误类型的变量上调用内建函数:
- 错误:
${123?upper_case}
。 - 原因:
?upper_case
是字符串内建函数,不能用于数字。 - 解决:确保内建函数应用于正确的数据类型。必要时先用
?string
转换:${(123)?string?upper_case}
。
- 错误:
忽略
null
值导致异常:- 错误:
${optionalField?length}
或${optionalField?first}
。 - 原因:
optionalField
为null
,调用其内建函数会抛出异常。 - 解决:使用
??
提供默认值或先用??
/?exists
检查。${(optionalField!"")?length}
<#if optionalField??>${optionalField?length}</#if>
- 错误:
混淆
?string
的不同用法:- 问题:
${date?string}
vs${date?string("yyyy-MM-dd")}
。 - 解决:明确
?string
的参数。无参数时是通用转换,有参数时是特定格式化(日期、数字)。
- 问题:
?map
和?filter
的 Lambda 语法错误:- 错误:
products?map(p -> p.name
(缺少右括号) 或products?filter(p -> p.price > 100
(缺少右括号)。 - 解决:注意语法
sequence?function(item -> expression)
,确保括号匹配。
- 错误:
对空序列使用
?first
/?last
:- 错误:
${emptyList?first}
。 - 原因:空序列没有首尾元素。
- 解决:先用
?has_content
检查:<#if list?has_content>${list?first}</#if>
。
- 错误:
?sort
修改原序列的误解:- 问题:认为
?sort
会改变原始序列。 - 解决:
?sort
返回新序列,原序列不变。需要时用<#assign>
接收:<#assign sortedTags = tags?sort>
。
- 问题:认为
4. 注意事项
?has_content
的妙用:- 对于字符串和序列,
?has_content
是检查“非空”的最佳选择,它等价于?? && ?length > 0
(字符串)或?? && ?size > 0
(序列)。
- 对于字符串和序列,
?string
的多面性:?string
本身是通用转换。?string("format")
用于日期/数字格式化。?string("trueText", "falseText")
用于布尔值。- 上下文决定其行为。
?c
的用途:?c
将数字、布尔值、日期等转换为计算机可读的、无本地化格式的字符串,非常适合在 JavaScript 或 JSON 中使用。
?exists
vs??
:variable?exists
: 变量在作用域中被定义(即使值为null
,也返回true
)。variable??
: 变量非null
。- 区别在于
null
值的处理。
链式调用 (Chaining):
- 内建函数可以链式调用:
variable?func1?func2?func3
。 - 执行顺序从左到右:先
func1
,结果传给func2
,依此类推。
- 内建函数可以链式调用:
性能:
- 大多数内建函数执行很快。
?sort
,?filter
,?map
对大型序列可能较慢。?join
对大型序列生成大字符串可能消耗内存。
5. 使用技巧
安全访问链:
<#-- 安全地访问嵌套属性 --> <#-- 方法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}
?then
内建函数:${condition?then("value1", "value2")}
是<#if condition>value1<#else>value2</#if>
的简洁替代,适合简单条件选择。
?chunk
用于布局:- 非常适合将列表数据按行(如每行 3 个)或分组展示,简化 HTML 表格或网格布局。
利用
?keys
/?values
遍历哈希表:<#list userInfo?keys as key> <p><strong>${key?cap_first}:</strong> ${userInfo[key]}</p> </#list>
?replace
的正则支持:?replace(pattern, replacement, "r")
支持正则表达式替换("r"
标志)。
6. 最佳实践与性能优化
数据预处理优先:
- 最佳实践:在 Java 后端完成复杂的转换、过滤、排序、聚合计算。将处理好的、适合展示的数据传递给模板。
- 优点:模板更简单、更易维护;后端逻辑更易测试;通常性能更好(JVM 优化)。
避免在模板中处理大数据集:
- 不要在模板中对包含成千上万条记录的序列进行
?sort
、?filter
等操作。 - 优化:在数据库或服务层完成分页、过滤和排序,模板只负责渲染当前页的数据。
- 不要在模板中对包含成千上万条记录的序列进行
合理使用缓存:
- 配置并利用 FreeMarker 的模板缓存。
- 对于计算成本高且结果稳定的内建函数调用(如复杂
?map
/?filter
),考虑在 Java 层缓存结果。
减少嵌套和复杂表达式:
- 避免过长的内建函数链或嵌套条件。
- 优化:使用
<#assign>
将中间结果存储在变量中,提高可读性。
使用
?c
保证数据一致性:- 当需要将数字、布尔值嵌入 JavaScript 或 JSON 时,务必使用
?c
,避免因本地化格式(如千位分隔符)导致解析错误。
- 当需要将数字、布尔值嵌入 JavaScript 或 JSON 时,务必使用
监控与分析:
- 关注模板渲染性能,特别是包含大量内建函数操作的模板。
- 使用性能分析工具定位瓶颈,优先考虑将耗时操作移至后端。
掌握 FreeMarker 内建函数是提升模板开发效率和质量的关键。遵循最佳实践,合理运用这些强大的工具,可以创建出既功能强大又易于维护的动态内容。