Freemarker模板实战指南
Freemarker是一个用Java编写的开源模板引擎,专为生成各种文本输出而设计。它采用MVC架构理念,严格分离业务逻辑与展示层,被广泛应用于动态Web页面生成、代码生成、邮件模板、报表生成等场景。
本文将从实战角度深入介绍Freemarker的核心特性、语法规则、最佳实践以及与Spring Boot的集成方案,帮助你快速掌握这个强大的模板引擎。
# 一、Freemarker模板简介
# 1、为什么选择Freemarker?
在众多模板引擎中,Freemarker凭借以下优势脱颖而出:
- 成熟稳定:自2000年发布以来,经过20多年的发展和优化,已在众多大型项目中得到验证
- 功能强大:提供丰富的内置函数、指令和宏,支持复杂的业务逻辑处理
- 性能卓越:模板预编译机制确保了高效的渲染性能
- 生态完善:与Spring、Struts等主流框架无缝集成,IDE支持良好
- 应用广泛:不仅限于Web开发,还可用于代码生成、文档生成、邮件模板等多种场景
# 2、典型应用场景
# 2.1、Web开发领域
- 动态页面渲染:电商平台的商品详情页、用户中心页面
- 邮件模板:注册确认邮件、订单通知、营销推广邮件
- 报表生成:Excel报表、PDF文档、Word文档生成
- API文档:自动生成RESTful API文档
# 2.2、代码生成领域
- MyBatis代码生成器:自动生成Mapper、Entity、Service层代码
- 项目脚手架:根据模板快速生成项目结构
- 数据库文档:自动生成数据库设计文档
# 2.3、配置文件生成
- Docker配置:动态生成Dockerfile、docker-compose.yml
- CI/CD配置:生成Jenkins Pipeline、GitLab CI配置
- 系统配置:根据环境变量生成不同的配置文件
# 二、Freemarker模板的基本语法
# 1、模板文件结构
Freemarker模板文件以.ftl
或.ftlh
(用于HTML)和.ftlx
(用于XML)为扩展名。模板文件可以包含静态文本、表达式、指令等元素,用于生成动态内容。
# 2、数据模型
数据模型是模板引擎从Java代码中获取的数据,通常采用键值对(key-value)的形式存储。Freemarker可以访问数据模型中的值,以生成动态内容。
# 3、注释
Freemarker支持单行和多行注释。注释内容不会出现在最终生成的文本中。
<#-- 单行注释 -->
<#--
多行注释
可以跨越多行
非常适合添加详细说明
-->
<!-- HTML注释会保留在输出中 -->
<#-- Freemarker注释不会出现在输出中 -->
最佳实践:
- 使用Freemarker注释(
<#-- -->
)来注释模板逻辑 - 使用HTML注释(
<!-- -->
)来注释最终需要在输出中保留的内容
# 4、变量
Freemarker支持变量,可以从数据模型中获取值并在模板中使用。
<#-- 基本变量输出 -->
${userName}
${user.age}
${product.price}
<#-- 带默认值的变量 -->
${nickName!"游客"}
${user.phone!"未填写"}
<#-- 判断变量是否存在 -->
<#if user??>
欢迎,${user.name}!
<#else>
请先登录
</#if>
<#-- 安全输出(避免null异常) -->
${user.address.city!"未知城市"}
# 三、表达式
# 1、基本表达式
# 1.1、变量表达式
- 简单变量
${userName}
${totalAmount}
在模板中,使用${}
包裹变量名以获取变量的值。
- 哈希表(对象属性访问)
${user.name}
${user.address.street}
${order.customer.email}
哈希表中的元素可以通过.
操作符访问,支持多级属性访问。
- 序列(数组/列表访问)
${colors[0]} <#-- 第一个元素 -->
${users[2].name} <#-- 第三个用户的名字 -->
${matrix[i][j]} <#-- 二维数组访问 -->
序列中的元素可以通过中括号[]
和索引访问,索引从0开始。
# 1.2、字符串表达式
"Hello, ${userName}!"
字符串可以使用双引号"
包裹,可以包含变量表达式。
可以使用r""表示原始字符串,原始字符串中的转义字符不会被转义。
<#assign rawString = r"Line 1\nLine 2">
${rawString}
${r"${foo}"}
${r"C:\foo\bar"}
输出:
Line 1\nLine 2
${foo}
C:\foo\bar
# 1.3、数字表达式
${totalPrice * 1.05}
数字表达式可以直接使用数字和运算符进行运算。
# 1.4、布尔表达式
${isPremiumMember}
布尔表达式通常表示条件判断的结果,值为true
或false
。
# 1.5、空值(默认值)表达式
${unknownVar!'N/A'}
使用!
操作符表示变量的默认值。
# 2、算术表达式
- 加法
${a + b}
- 减法
${a - b}
- 乘法
${a * b}
- 除法
${a / b}
- 取余
${a % b}
- 求幂
${a ^ b}
# 3、比较表达式
- 等于
${a == b}
- 不等于
${a != b}
- 大于
${a > b}
- 小于
${a < b}
- 大于等于
${a >= b}
- 小于等于
${a <= b}
# 4、逻辑表达式
- 与(AND)
${a && b}
- 或(OR)
${a || b}
- 非(NOT)
${!a}
# 5、范围表达式
<#list 1..5 as number>
${number}
</#list>
使用范围迭代可以遍历一个数字范围,两个数字之间用两个点..
表示。
范围迭代的其他用法:
<
: 独占结束。比如说1..<4
给出[1,2, 3]
!
: 同<
号*
: 长度限制范围。比如10..*4
给出[10, 11, 12, 13]
,10..*-4
给出[10,9,8]
- 除了正序之外,还支持倒序。比如
5..1
给出[5,4,3,2,1]
# 6、切片
字符串和序列都可以配合范围表达式来切片,比如对于一个字符串:
<#assign s = "ABCDEF">
${s[2..3]}
${s[2..<4]}
${s[2..*3]}
${s[2..*100]}
${s[2..]}
将输出:
CD
CD
CDE
CDEF
CDEF
字符串可以看成是字符的序列,对于序列来说,用法和上面一致。
# 7、方法调用
${repeat("Foo", 3)}
repeat 是一个方法变量
# 8、缺省值和缺失值判断
使用叹号可以设置变量缺省值:
${mouse!"No mouse."}
<#assign mouse="Jerry">
${mouse!"No mouse."}
输出:
No mouse.
Jerry
判断一个值是否存在,使用两个问号:
<#if mouse??>
Mouse found
<#else>
No mouse found
</#if>
Creating mouse...
<#assign mouse = "Jerry">
<#if mouse??>
Mouse found
<#else>
No mouse found
</#if>
输出:
No mouse found
Creating mouse...
Mouse found
# 9、其他
- 可以使用括号进行表达式分组
- 表达式中的空格会被忽略
- 算数运算符的优先级和C、Java等同
# 四、Freemarker模板指令
# 1、条件指令
# 1.1、if指令
if
指令用于条件判断,根据条件的真假决定是否执行某段代码。
<#-- 基本用法 -->
<#if user.age >= 18>
<p>您已成年,可以访问所有内容</p>
</#if>
<#-- 判断对象是否存在 -->
<#if user??>
<p>欢迎回来,${user.name}!</p>
</#if>
<#-- 判断集合是否为空 -->
<#if products?has_content>
<ul>
<#list products as product>
<li>${product.name}</li>
</#list>
</ul>
</#if>
<#-- 多条件判断 -->
<#if user?? && user.vipLevel gt 3>
<p>尊贵的VIP用户,享受专属优惠!</p>
</#if>
# 1.2、elseif和else指令
elseif
和else
指令可以与if
指令配合使用,实现多条件判断。
<#if condition1>
// condition1为真时执行的代码
<#elseif condition2>
// condition1为假且condition2为真时执行的代码
<#else>
// 所有条件均为假时执行的代码
</#if>
# 1.3、switch指令
switch
指令用于多条件分支判断。
<#switch value>
<#case refValue1>
...
<#break>
<#case refValue2>
...
<#break>
...
<#case refValueN>
...
<#break>
<#default>
...
</#switch>
# 2、循环指令
# 2.1、list指令
list
指令用于遍历序列或哈希,实现循环输出。
<#-- 基本列表遍历 -->
<#list users as user>
<tr>
<td>${user_index + 1}</td> <#-- 索引从0开始 -->
<td>${user.name}</td>
<td>${user.email}</td>
</tr>
</#list>
<#-- 带有else的列表(当列表为空时) -->
<#list products as product>
<div class="product">
<h3>${product.name}</h3>
<p>价格:¥${product.price}</p>
</div>
<#else>
<p>暂无产品信息</p>
</#list>
<#-- 使用sep分隔符 -->
<#list colors as color>
${color}<#sep>, </#sep>
</#list>
<!-- 输出:红色, 蓝色, 绿色 -->
<#-- 带条件的break和continue -->
<#list items as item>
<#if item.status == "deleted">
<#continue> <#-- 跳过已删除项 -->
</#if>
<#if item_index >= 10>
<#break> <#-- 只显示前10项 -->
</#if>
<li>${item.name}</li>
</#list>
<#-- 遍历Map -->
<#list userMap as key, value>
<p>${key}: ${value}</p>
</#list>
循环变量内置属性:
item_index
: 当前索引(从0开始)item_has_next
: 是否有下一个元素item_counter
: 计数器(从1开始)item?is_first
: 是否是第一个元素item?is_last
: 是否是最后一个元素item?is_even_item
: 是否是偶数项item?is_odd_item
: 是否是奇数项
# 3、包含与宏指令
# 3.1、include指令
include
指令用于在当前模板中包含另一个模板的内容。
<#include "path/to/template.ftl">
指令有三个参数:
parse
默认为true,表示解析模板中的指令。如果为false,则模板中的指令不会被解析,而是原样输出。encoding
默认为UTF-8,表示模板的编码格式。ignore_missing
默认为false,表示如果模板不存在则抛出异常。如果为true,则不抛出异常,而是原样输出。
# 3.2、import指令
import
指令用于引入另一个模板的宏(macro)。
<#import "/libs/commons.ftl" as com>
<@com.copyright date="1999-2002"/>
# 3.3、macro指令
macro
指令用于定义宏,宏可以包含一段可重复使用的代码片段。
<#macro macroName param1 param2>
// 宏的内容
</#macro>
调用宏的方法如下:
<@macroName param1=value1 param2=value2/>
示例:
<#macro shout text>
${text?upper_case}!
</#macro>
---
<@shout text="hello"/>
nested
指令允许在宏中包含调用宏时传入的内容,实现更高级的代码复用。
<#macro wrapper>
<div>
<#nested/>
</div>
</#macro>
<@wrapper>
<p>这是嵌套内容</p>
</@>
# 4、变量与函数指令
# 4.1、assign指令
assign
指令用于在模板中定义变量。
<#assign variableName = value>
# 4.2、local指令
local
指令用于在模板中定义局部变量,和assign指令类似,但是它创建或替换局部变量。这些变量仅在当前指令范围内有效。
<#local variableName = value>
# 4.3、global指令
global
指令用于在模板中定义全局变量,和assign指令类似,但是它创建或替换全局变量。这些变量在整个模板中都有效。
<#global variableName = value>
# 4.4、function指令
定义一个函数。
<#function avg x y>
<#return (x + y) / 2>
</#function>
${avg(10, 20)}
如果没有返回值,则返回null。
<#function avg nums...>
<#local sum = 0>
<#list nums as num>
<#local sum += num>
</#list>
<#if nums?size != 0>
<#return sum / nums?size>
</#if>
</#function>
${avg(10, 20)}
${avg(10, 20, 30, 40)}
${avg()!"N/A"}
# 5、模板设置指令
# 5.1、setting指令
设置当前模板的一些属性,如编码格式、日期格式、时区等。
<#setting locale="de_DE">
${1.2}
<#setting locale="en_US">
${1.2}
输出:
1,2
1.2
在 Freemarker 中,ftl 和 setting 都是指令,用于控制模板引擎的行为和配置。
setting 指令的属性有:
- locale:设置模板使用的语言环境(Locale)。
- number_format:设置数字格式化的模式。
- time_format:设置时间格式化的模式。
- date_format:设置日期格式化的模式。
- datetime_format:设置日期时间格式化的模式。
- boolean_format:设置布尔值格式化的模式。
- classic_compatible:设置是否启用兼容模式,以支持 Freemarker 1.x 的语法。
- whitespace_stripping:设置是否启用模板中的空白字符剥离。
- strict_syntax:设置是否启用严格的语法检查。
- tag_syntax:设置模板中使用的标签语法风格。
- naming_convention:设置模板中命名约定的风格。
- recognized_environments:设置模板中识别的运行环境。
- auto_import:设置自动导入的命名空间和类。
- recognized_macro_library_files:设置模板中识别的宏库文件。
- boolean_format:true_value/false_value:设置布尔值格式化时的真值和假值显示文本。
- object_wrapper:设置用于包装模板中的对象的对象包装器。
- interpolation_syntax:设置插值语法的风格。
- output_encoding:设置模板的输出编码。
# 6、输出控制指令
# 6.1、escape指令
escape
指令用于转义特殊字符,防止跨站脚本攻击(XSS)等安全问题。
<#escape x as x?html>
// 转义后的内容,所有特殊字符将被替换为HTML实体
</#escape>
# 6.2、autoesc指令
autoesc 是 Freemarker 中的一个指令,用于自动转义输出值以提供基本的防御机制,防止跨站点脚本攻击(XSS)。
它会将输出值中的特殊字符(如 <, >, ', " 等)替换为对应的HTML实体,以确保在HTML上下文中显示时不会被解释为标签或脚本。
<#autoesc>
${"&"}
...
${"&"}
</#autoesc>
输出:
&
...
&
# 6.3、noparse指令
相当于HTML中pre标签,会原样输出模板中的内容。
<#noparse>
<#list animals as animal>
<tr><td>${animal.name}<td>${animal.price} Euros
</#list>
</#noparse>
输出:
<#list animals as animal>
<tr><td>${animal.name}<td>${animal.price} Euros
</#list>
# 6.4、compress指令
可以移除每一行前后的空白。
<#assign x = " moo \n\n ">
(<#compress>
1 2 3 4 5
${moo}
test only
I said, test only
</#compress>)
输出:
(1 2 3 4 5
moo
test only
I said, test only)
# 6.5、t/lt/rt指令
这些指令指示FreeMarker忽略标记行中的某些空白。
t
忽略标记行中的所有空白。lt
忽略标记行中的左侧空白。rt
忽略标记行中的右侧空白。
--
1 <#t>
2<#t>
3<#lt>
4
5<#rt>
6
--
输出:
--
1 23
4
5 6
--
# 7、调试指令
# 7.1、stop指令
stop
指令用于立即停止处理当前模板,不再生成任何输出。这在某些特殊情况下可能会用到,例如处理错误或提前终止模板渲染。
<#stop>
# 7.2、attempt指令和recover指令
attempt
指令和recover
指令用于错误处理,它们可以捕获模板渲染过程中出现的异常,并提供友好的错误信息。
<#attempt>
// 可能引发异常的代码
<#recover>
// 异常处理代码
</#attempt>
Freemarker功能非常丰富,还有许多其他的指令和功能。要深入了解所有的指令和功能,请参考Freemarker官方文档-指令集合 (opens new window)。
# 8、自定义指令
除了Freemarker内置的指令之外,还可以编写自定义指令来扩展功能。
自定义指令需要实现freemarker.template.TemplateDirectiveModel
接口,然后在Java代码中将自定义指令添加到数据模型中。
public class CustomDirective implements TemplateDirectiveModel {
@Override
public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
throws TemplateException, IOException {
// 获取参数
String name = ((SimpleScalar) params.get("name")).getAsString();
// 执行指令体
StringWriter writer = new StringWriter();
body.render(writer);
String content = writer.toString();
// 输出结果
String result = String.format("Hello, %s! Your message is: %s", name, content);
env.getOut().write(result);
}
}
在模板中使用自定义指令:
<@hello name="Alice">Hello, world!</@hello>
输出:
Hello, Alice! Your message is: Hello, world!
# 五、内置函数
Freemarker提供了许多内置函数,用于处理字符串、数字、日期、序列等数据类型。这些内置函数可以对变量进行操作,以生成所需的输出。
内置函数的调用形式是:
${变量名?内置函数}
${变量名?函数1?函数2} <#-- 链式调用 -->
# 1、常用内置函数示例
<#-- 字符串处理 -->
${name?upper_case} <#-- 转大写: JOHN -->
${name?lower_case} <#-- 转小写: john -->
${title?cap_first} <#-- 首字母大写: Hello world -->
${email?contains("@")?string} <#-- 是否包含: true/false -->
${phone?replace("-", "")} <#-- 替换: 13812345678 -->
${text?substring(0, 10)} <#-- 截取: 前10个字符 -->
${description?truncate(50)} <#-- 截断并添加... -->
${name?length} <#-- 长度: 4 -->
${url?url} <#-- URL编码 -->
${html?html} <#-- HTML转义 -->
<#-- 数字格式化 -->
${price?string("0.00")} <#-- 格式化: 19.99 -->
${price?string.currency} <#-- 货币: ¥19.99 -->
${percent?string.percent} <#-- 百分比: 85% -->
${count?string("000")} <#-- 补零: 007 -->
${amount?round} <#-- 四舍五入 -->
${value?abs} <#-- 绝对值 -->
<#-- 日期处理 -->
${.now?string("yyyy-MM-dd")} <#-- 当前日期: 2024-01-15 -->
${birthday?date} <#-- 只取日期部分 -->
${createTime?time} <#-- 只取时间部分 -->
${updateTime?datetime} <#-- 日期时间 -->
${date?string("EEEE")} <#-- 星期几: Monday -->
<#-- 集合操作 -->
${users?size} <#-- 集合大小: 10 -->
${list?first} <#-- 第一个元素 -->
${list?last} <#-- 最后一个元素 -->
${list?reverse} <#-- 反转列表 -->
${list?sort} <#-- 排序 -->
${list?seq_contains("item")?string} <#-- 是否包含元素 -->
<#-- 空值处理 -->
${name!"默认值"} <#-- 默认值 -->
${user.name!"匿名"}
<#if email??> <#-- 判断是否存在 -->
${email}
</#if>
${list!?size} <#-- 空安全调用 -->
# 2、字符串函数
以下是各个字符串函数的参数和功能介绍:
boolean(str)
:将一个字符串转换为布尔类型。参数str
是要转换的字符串。如果字符串是true
、yes
、y
、t
、1
,则返回true
,否则返回false
。cap_first(str)
:将一个字符串的首字母大写。参数str
是要大写首字母的字符串。c(str)
:将一个对象转换成字符串。参数str
是要转换的对象。cn(str)
:将一个对象转换成字符串,并将null
值转换为空字符串。参数str
是要转换的对象。c_lower_case(str)
:将一个字符串的第一个单词转换成小写字母。参数str
是要转换的字符串。c_upper_case(str)
:将一个字符串的第一个单词转换成大写字母。参数str
是要转换的字符串。capitalize(str)
:将一个字符串的所有单词的首字母大写,注意和cap_first的差异。参数str
是要大写首字母的字符串。chop_linebreak(str)
:去除一个字符串末尾的换行符(如果有)。参数str
是要去除换行符的字符串。contains(str, search)
:判断一个字符串是否包含指定的子字符串。参数str
是要判断的字符串,search
是要查找的子字符串。date(date, format)
:将一个日期对象格式化成字符串。参数date
是要格式化的日期对象,format
是日期格式模板。time(time, format)
:将一个时间对象格式化成字符串。参数time
是要格式化的时间对象,format
是时间格式模板。datetime(datetime, format)
:将一个日期时间对象格式化成字符串。参数datetime
是要格式化的日期时间对象,format
是日期时间格式模板。ends_with(str, suffix)
:判断一个字符串是否以指定的后缀结尾。参数str
是要判断的字符串,suffix
是要查找的后缀。ensure_ends_with(str, suffix)
:确保一个字符串以指定的后缀结尾,如果没有则添加。参数str
是要处理的字符串,suffix
是要添加的后缀。ensure_starts_with(str, prefix)
:确保一个字符串以指定的前缀开头,如果没有则添加。参数str
是要处理的字符串,prefix
是要添加的前缀。esc(str)
:对一个字符串进行 HTML 和 XML 转义,将一些特殊字符转换成 HTML 或 XML 实体。参数str
是要转义的字符串。groups(str, pattern)
:在一个字符串中查找指定正则表达式的匹配,并返回匹配的分组。参数str
是要查找的字符串,pattern
是正则表达式。html(str)
:对一个字符串进行 HTML 转义,将一些特殊字符转换成 HTML 实体。参数str
是要转义的字符串。index_of(str, search)
:查找一个字符串中指定子字符串第一次出现的位置。参数str
是要查找的字符串,search
是要查找的子字符串。j_string(str)
:对一个字符串进行 JSON 转义,将一些特殊字符转换成 JSON 字符串中的转义字符。参数str
是要转义的字符串。js_string(str)
:对一个字符串进行 JavaScript 转义,将一些特殊字符转换成 JavaScript 字符串中的转义字符。参数str
是要转义的字符串。json_string(obj)
:将一个对象转换成 JSON 字符串。参数obj
是要转换的对象。keep_after(str, search)
:保留一个字符串中指定子字符串后面的内容。参数str
是要处理的字符串,search
是要保留的子字符串。keep_after_last(str, search)
:保留一个字符串中最后一个指定子字符串后面的内容。参数str
是要处理的字符串,search
是要保留的子字符串。keep_before(str, search)
:保留一个字符串中指定子字符串前面的内容。参数str
是要处理的字符串,search
是要保留的子字符串。keep_before_last(str, search)
:保留一个字符串中最后一个指定子字符串前面的内容。参数str
是要处理的字符串,search
是要保留的子字符串。last_index_of(str, search)
:查找一个字符串中指定子字符串最后一次出现的位置。参数str
是要查找的字符串,search
是要查找的子字符串。left_pad(str, width, pad)
:在一个字符串左侧添加指定的填充字符,使其达到指定的宽度。参数str
是要填充的字符串,width
是要填充到的宽度,pad
是填充字符。length(str)
:获取一个字符串的长度。参数str
是要获取长度的字符串。lower_case(str)
:将一个字符串转换成小写字母。参数str
是要转换的字符串。matches(str, pattern)
:判断一个字符串是否匹配指定的正则表达式。参数str
是要判断的字符串,pattern
是正则表达式。no_esc(str)
:对一个字符串进行无转义输出,不进行任何转义。参数str
是要输出的字符串。number(str)
:将一个字符串转换成数字。参数str
是要转换的字符串。replace(str, search, replace)
:将一个字符串中所有出现的目标字符串替换成另一个字符串。参数str
是原始字符串,search
是要被替换的目标字符串,replace
是替换目标字符串的字符串。right_pad(str, width, pad)
:在一个字符串右侧添加指定的填充字符,使其达到指定的宽度。参数str
是要填充的字符串,width
是要填充到的宽度,pad
是填充字符。remove_beginning(str, remove)
:去除一个字符串开头的指定子字符串。参数str
是要处理的字符串,remove
是要去除的子字符串。remove_ending(str, remove)
:去除一个字符串末尾的指定子字符串。参数str
是要处理的字符串,remove
是要去除的子字符串。rtf(str)
:对一个字符串进行 RTF 转义,将一些特殊字符转换成 RTF 控制字符。参数str
是要转义的字符串。split(str, delimiter)
:将一个字符串按照指定的分隔符拆分成一个字符串列表。参数str
是要拆分的字符串,delimiter
是分隔符。starts_with(str, prefix)
:判断一个字符串是否以指定的前缀开头。参数str
是要判断的字符串,prefix
是要查找的前缀。string(str)
:将一个对象转换成字符串。参数str
是要转换的对象。substring(str, beginIndex, endIndex)
:获取一个字符串的子串。参数str
是要获取子串的字符串,beginIndex
是起始下标(包括该下标的字符),endIndex
是结束下标(不包括该下标的字符)。trim(str)
:去除一个字符串两侧的空格。参数str
是要去除空格的字符串。truncate(str, maxLength)
:将一个字符串截断到指定的最大长度。参数str
是要截断的字符串,maxLength
是最大长度。truncate_auto(str, maxLength, suffix)
:将一个字符串截断到指定的最大长度,并添加指定的后缀。如果字符串本身已经小于等于最大长度,则不进行截断。参数str
是要截断的字符串,maxLength
是最大长度,suffix
是要添加的后缀。truncate_words(str, maxLength, suffix)
:将一个字符串按照空格拆分成单词列表,将单词列表缩略到指定的最大长度,并添加指定的后缀。参数str
是要处理的字符串,maxLength
是最大长度,suffix
是要添加的后缀。uncap_first(str)
:将一个字符串的首字母转换成小写字母。参数str
是要转换的字符串。upper_case(str)
:将一个字符串转换成大写字母。参数str
是要转换的字符串。url(str)
:对一个字符串进行 URL 转义,将一些特殊字符转换成 URL 编码。参数str
是要转义的字符串。url_path(str)
:对一个字符串进行 URL 路径转义,将一些特殊字符转换成 URL 编码。参数str
是要转义的字符串。word_list(str)
:将一个字符串按照空格拆分成单词列表。参数str
是要拆分的字符串。
需要注意,上面说的str一般表示的是表达式的值。
对于replace、split、ensure_starts_with、keep_after、keep_before等函数会用到flags参数,flags参数是一个字符串,可以包含以下字符:
r
:要查找的子字符串是一个正则表达式。i
:在匹配时忽略大小写。m
:启用多行模式。^ 和 $ 可以匹配每一行的开头和结尾。s
:启用点号(.)匹配任意字符,包括换行符。c
:允许在正则表达式中使用空格和注释。f
:仅首次。即,仅替换/查找等第一次出现的内容。
这些标志可以根据匹配需求的不同进行组合使用。例如,使用 i
标志可以实现不区分大小写的匹配,而使用 m
标志可以实现多行模式的匹配。
函数比较多,对于有些不理解的函数,参考string 内置函数 (opens new window),上面有对各个函数介绍和示例。
# 3、数字函数
abs(num)
:返回一个数的绝对值。参数num
是要求绝对值的数字。c(num)
:将一个对象转换成数值类型。参数num
是要转换的对象。cn(num)
:将一个对象转换成数值类型,并将null
值转换为 0。参数num
是要转换的对象。is_infinite(num)
:判断一个数是否为无穷大。参数num
是要判断的数字。is_nan(num)
:判断一个数是否为 NaN(Not a Number)。参数num
是要判断的数字。lower_abc(num)
:将一个数转换成小写字母表中对应的字母。参数num
是要转换的数字,转换规则为 1 对应a
,2 对应b
,以此类推。round(num, scale, roundingMode)
:对一个数进行四舍五入。参数num
是要四舍五入的数字,scale
是小数点后保留的位数,roundingMode
是取整模式,可以是halfUp
、halfDown
、halfEven
、up
、down
中的一种。floor(num)
:对一个数进行向下取整。参数num
是要取整的数字。ceiling(num)
:对一个数进行向上取整。参数num
是要取整的数字。string(num)
:将一个对象转换成字符串。参数num
是要转换的对象,通常为数值类型。upper_abc(num)
:将一个数转换成大写字母表中对应的字母。参数num
是要转换的数字,转换规则为 1 对应A
,2 对应B
,以此类推。
# 4、日期函数
date(date)
:从一个日期/时间/日期时间值中提取出日期部分。参数date
是要提取日期部分的日期/时间/日期时间值。time(date)
:从一个日期/时间/日期时间值中提取出时间部分。参数date
是要提取时间部分的日期/时间/日期时间值。datetime(date)
:将一个日期/时间/日期时间值转换成日期时间格式的字符串。参数date
是要转换的日期/时间/日期时间值。date_if_unknown(date, fallback)
:从一个日期/时间/日期时间值中提取出日期部分,如果提取不到,则返回指定的备选值。参数date
是要提取日期部分的日期/时间/日期时间值,fallback
是备选值。time_if_unknown(date, fallback)
:从一个日期/时间/日期时间值中提取出时间部分,如果提取不到,则返回指定的备选值。参数date
是要提取时间部分的日期/时间/日期时间值,fallback
是备选值。datetime_if_unknown(date, fallback)
:将一个日期/时间/日期时间值转换成日期时间格式的字符串,如果转换失败,则返回指定的备选值。参数date
是要转换的日期/时间/日期时间值,fallback
是备选值。iso_...
:ISO 格式化日期/时间/日期时间值的函数。可以有以下变种:iso_date(date)
:将一个日期/时间/日期时间值转换成 ISO 8601 格式的日期字符串,例如2019-12-31
。iso_time(date)
:将一个日期/时间/日期时间值转换成 ISO 8601 格式的时间字符串,例如12:34:56.789
。iso_datetime(date)
:将一个日期/时间/日期时间值转换成 ISO 8601 格式的日期时间字符串,例如2019-12-31T12:34:56.789+08:00
。
string(date)
:将一个日期/时间/日期时间值转换成字符串。参数date
是要转换的日期/时间/日期时间值。
# 5、布尔函数
c(bool)
:将一个对象转换成布尔类型。参数bool
是要转换的对象。cn(bool)
:将一个对象转换成布尔类型,并将null
值转换为false
。参数bool
是要转换的对象。string(bool)
:将一个对象转换成字符串。参数bool
是要转换的对象,通常为布尔类型。then(condition, thenValue, elseValue)
:根据条件返回指定的值。如果条件为真,则返回thenValue
,否则返回elseValue
。参数condition
是要判断的条件,可以是布尔类型或布尔表达式;thenValue
是条件为真时返回的值;elseValue
是条件为假时返回的值。
# 6、序列函数
chunk(seq, chunkSize)
:将一个序列按照指定的大小拆分成多个子序列。参数seq
是要拆分的序列,chunkSize
是每个子序列的大小。drop_while(seq, condition)
:从一个序列中删除满足条件的前缀,直到遇到第一个不满足条件的元素。参数seq
是要处理的序列,condition
是一个布尔表达式,表示要删除的元素应该满足的条件。filter(seq, condition)
:从一个序列中筛选出满足条件的元素,返回一个新的序列。参数seq
是要筛选的序列,condition
是一个布尔表达式,表示要筛选的元素应该满足的条件。first(seq)
:返回一个序列的第一个元素。参数seq
是要取第一个元素的序列。join(seq, separator)
:将一个序列中的所有元素用指定的分隔符连接成一个字符串。参数seq
是要连接的序列,separator
是分隔符。last(seq)
:返回一个序列的最后一个元素。参数seq
是要取最后一个元素的序列。map(seq, transform)
:对一个序列中的每个元素应用指定的转换函数,返回一个新的序列。参数seq
是要转换的序列,transform
是转换函数。min(seq)
:返回一个序列中的最小值。参数seq
是要查找最小值的序列。max(seq)
:返回一个序列中的最大值。参数seq
是要查找最大值的序列。reverse(seq)
:将一个序列中的元素顺序颠倒,返回一个新的序列。参数seq
是要翻转的序列。seq_contains(seq, value)
:判断一个序列是否包含指定的值。参数seq
是要查找的序列,value
是要查找的值。seq_index_of(seq, value)
:返回一个序列中第一次出现指定值的下标,如果没有找到则返回 -1。参数seq
是要查找的序列,value
是要查找的值。seq_last_index_of(seq, value)
:返回一个序列中最后一次出现指定值的下标,如果没有找到则返回 -1。参数seq
是要查找的序列,value
是要查找的值。size(seq)
:返回一个序列中元素的个数。参数seq
是要计算大小的序列。sort(seq)
:对一个序列进行升序排序,返回一个新的序列。参数seq
是要排序的序列。sort_by(seq, transform)
:对一个序列中的元素进行转换,然后根据转换结果进行升序排序,返回一个新的序列。参数seq
是要排序的序列,transform
是转换函数,它将序列中的每个元素转换为排序用的值。转换函数可以是一个 lambda 表达式或是一个自定义函数。take_while(seq, condition)
:从一个序列中取出满足条件的前缀,直到遇到第一个不满足条件的元素。参数seq
是要处理的序列,condition
是一个布尔表达式,表示要取出的元素应该满足的条件。
# 7、哈希表函数
keys(map)
:返回一个哈希表中所有键组成的列表。参数map
是要获取键列表的哈希表。values(map)
:返回一个哈希表中所有值组成的列表。参数map
是要获取值列表的哈希表。
对于list指令,也可以把键值一次性赋值:<#list attrs as key, value>...<#list>
# 8、循环变量函数
list指令对应着Java的foreach。对于其中的循环变量,有如下函数:
counter
:返回当前循环的计数器值,从 1 开始计数。has_next
:判断当前循环是否有下一个元素,返回一个布尔值。index
:返回当前循环的索引值,从 0 开始计数。is_even_item
:判断当前循环的计数器值是否为偶数,返回一个布尔值。is_first
:判断当前循环是否为第一个元素,返回一个布尔值。is_last
:判断当前循环是否为最后一个元素,返回一个布尔值。is_odd_item
:判断当前循环的计数器值是否为奇数,返回一个布尔值。item_cycle(list)
:按照顺序循环遍历一个列表中的所有元素,并在列表的末尾重新开始。参数list
是要循环遍历的列表。item_parity
:判断当前循环的计数器值是偶数还是奇数,并返回对应的字符串"even"
或"odd"
。item_parity_cap
:判断当前循环的计数器值是偶数还是奇数,并返回对应的字符串"Even"
或"Odd"
,首字母大写。
# 9、XML节点函数
ancestors(node)
:返回一个节点的所有祖先节点。参数node
是要获取祖先节点的节点。children(node)
:返回一个节点的所有子节点。参数node
是要获取子节点的节点。node_name(node)
:返回一个节点的名称。参数node
是要获取名称的节点。next_sibling(node)
:返回一个节点的下一个同级节点。参数node
是要获取下一个同级节点的节点。node_namespace(node)
:返回一个节点的命名空间。参数node
是要获取命名空间的节点。node_type(node)
:返回一个节点的类型。参数node
是要获取类型的节点。parent(node)
:返回一个节点的父节点。参数node
是要获取父节点的节点。previous_sibling(node)
:返回一个节点的上一个同级节点。参数node
是要获取上一个同级节点的节点。root(node)
:返回一个节点所在的文档的根节点。参数node
是要获取根节点的节点。
# 10、switch函数
独立于类型的函数只有switch一个。
switch(case1, result1, case2, result2, ... caseN, resultN, defaultResult)
:按照指定的值或表达式进行匹配,返回匹配到的结果。
用法:
<#list ['r', 'w', 'x', 's'] as flag>
${flag?switch('r', 'readable', 'w' 'writable', 'x', 'executable', 'unknown flag: ' + flag)}
</#list>
# 11、专家函数
以下是一些通常不该用到的内置函数,但是在调试或者高级宏中可能会用到他们。
absolute_template_name(template)
:返回模板的绝对路径。参数template
是模板名称。api(name)
:在模板中引入 Java API 类。参数name
是 Java 类的名称。has_api(name)
:判断是否存在指定的 Java API 类。参数name
是 Java 类的名称。byte(value)
:将一个值转换为字节类型。double(value)
:将一个值转换为双精度浮点数类型。float(value)
:将一个值转换为单精度浮点数类型。int(value)
:将一个值转换为整型。long(value)
:将一个值转换为长整型。short(value)
:将一个值转换为短整型。eval(expression)
:执行一个表达式,并返回表达式的值。参数expression
是要执行的表达式。eval_json(expression)
:将 JSON 字符串转换为对象,并执行一个表达式。参数expression
是要执行的表达式,它可以包括 JSON 对象的属性和方法调用。has_content(value)
:判断一个值是否为空。如果值为字符串类型,则忽略空格和换行符,只要包含任意字符就返回true
;如果值为集合类型,则只要包含任意元素就返回true
;如果值为null
,则返回false
。interpret(expression)
:将一个字符串解析为模板,并执行解析后的模板。参数expression
是要解析的字符串。is_...(value)
:一组函数,用于判断一个值是否属于某种类型,包括is_boolean
、is_date
、is_hash
、is_list
、is_macro
、is_node
、is_number
、is_sequence
、is_string
等。markup_string(string)
:将一个字符串解析为标记字符串,并返回标记字符串对象。参数string
是要解析的字符串。namespace(prefix)
:返回指定前缀的命名空间。参数prefix
是命名空间前缀。new(className, args)
:创建一个新的对象。参数className
是对象的类名,args
是对象的构造函数参数。number_to_date(value, pattern)
、number_to_time(value, pattern)
、number_to_datetime(value, pattern)
:将一个数字转换为日期、时间或日期时间,并返回格式化后的字符串。参数value
是要转换的数字,pattern
是日期、时间或日期时间的格式化字符串。sequence(start, end)
:创建一个指定范围的整数序列。参数start
是序列的起始值,end
是序列的结束值(不包含)。with_args(arguments, expression)
:执行一个表达式,并将参数传递给表达式。参数arguments
是一个列表,包含参数的名称和值,expression
是要执行的表达式。with_args_last(arguments, expression)
:执行一个表达式,并将参数传递给表达式。与with_args
函数不同之处在于,最后一个参数的值会被作为可变参数传递给表达式。参数arguments
是一个列表,包含参数的名称和值,最后一个参数的名称应为...
,expression
是要执行的表达式。
# 六、特殊变量引用
特殊变量是FreeMarker引擎定义的变量 本身。要访问它们,请使用.variable_name
语法。例如,你不能简单地写version
;你必须写.version
。
以下是特殊变量列表:
args
: 表示当前模板调用的参数列表。该变量引用的是一个包含所有参数的 Map 对象。在模板中可以使用${args.name}
的方式获取特定参数的值。current_template_name
: 表示当前模板的名称。该变量引用的是一个字符串,表示当前模板的文件名或其他标识符。data_model
: 表示模板使用的数据模型。该变量引用的是一个 Map 对象,其中包含了模板需要的所有数据。在模板中可以使用${data_model.name}
的方式获取特定数据的值。global
: 表示全局数据模型。该变量引用的是一个 Map 对象,其中包含了所有模板都可以使用的全局数据。在模板中可以使用${global.name}
的方式获取特定全局数据的值。locale
: 表示当前模板的区域设置。该变量引用的是一个字符串,表示当前模板的区域设置,例如 "en_US" 或 "zh_CN" 等。main
: 表示主模板。该变量引用的是一个布尔值,表示当前模板是否为主模板。如果是,则返回 true,否则返回 false。namespace
: 表示当前命名空间。该变量引用的是一个字符串,表示当前模板的命名空间。output_encoding
: 表示当前输出编码。该变量引用的是一个字符串,表示当前模板输出的编码格式,例如 "UTF-8" 或 "ISO-8859-1" 等。get_optional_template
: 表示获取可选模板的函数。该变量引用的是一个函数对象,可以使用 `${get_optional_template(name)} 的方式获取特定名称的可选模板。如果指定的模板不存在,则返回 null。incompatible_improvements
: 表示不兼容的改进版本号。该变量引用的是一个字符串,表示当前模板使用的 FreeMarker 不兼容的改进版本号。namespace_prefix
: 表示命名空间前缀。该变量引用的是一个字符串,表示当前模板的命名空间前缀。object_wrapper
: 表示对象包装器。该变量引用的是一个对象包装器,用于将 Java 对象转换为模板可以识别的模型对象。auto_flush
: 表示自动刷新。该变量引用的是一个布尔值,表示当前模板的自动刷新设置。如果设置为 true,则 FreeMarker 会在缓冲区满或模板执行完成后自动刷新输出。output_format
: 表示输出格式。该变量引用的是一个输出格式对象,用于控制模板的输出格式。time_zone
: 表示时区。该变量引用的是一个时区对象,用于控制模板的时间格式和解析行为。url_escaping_charset
: 存储应该用于URL转义的字符集。如果这个变量不存在,这意味着没有人指定应该用于URL编码。在这种情况下,内置使用由output_encoding指定的字符集用于URL编码的特殊变量;自定义机制可能遵循同样的逻辑。number_format
: 表示数字格式。该变量引用的是一个数字格式对象,用于控制模板中数字的格式化方式。boolean_format
: 表示布尔值格式。该变量引用的是一个布尔值格式对象,用于控制模板中布尔值的格式化方式。date_format
: 表示日期格式。该变量引用的是一个日期格式对象,用于控制模板中日期的格式化方式。time_format
: 表示时间格式。该变量引用的是一个时间格式对象,用于控制模板中时间的格式化方式。datetime_format
: 表示日期时间格式。该变量引用的是一个日期时间格式对象,用于控制模板中日期时间的格式化方式。
# 七、Freemarker模板与Java集成
# 1、Java环境配置
要使用Freemarker模板引擎,首先需要将其添加到Java项目中。在Maven项目中,可以将以下依赖添加到pom.xml
文件中:
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
对于Gradle项目,将以下依赖添加到build.gradle
文件中:
implementation 'org.freemarker:freemarker:2.3.31'
# 2、Java代码实现
# 2.1、完整示例:生成HTML邮件
下面通过一个完整的示例展示如何在Java中使用Freemarker生成HTML邮件:
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.Version;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class EmailTemplateService {
private Configuration cfg;
public EmailTemplateService() {
// 初始化Configuration
cfg = new Configuration(new Version("2.3.31"));
cfg.setClassForTemplateLoading(this.getClass(), "/templates");
cfg.setDefaultEncoding(StandardCharsets.UTF_8.name());
cfg.setLogTemplateExceptions(false);
}
public String generateOrderEmail(Order order) throws Exception {
// 准备数据模型
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("order", order);
dataModel.put("customer", order.getCustomer());
dataModel.put("items", order.getItems());
dataModel.put("totalAmount", calculateTotal(order.getItems()));
dataModel.put("currentDate", new Date());
// 加载模板
Template template = cfg.getTemplate("order-confirmation.ftl");
// 渲染模板
StringWriter out = new StringWriter();
template.process(dataModel, out);
return out.toString();
}
private double calculateTotal(List<OrderItem> items) {
return items.stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum();
}
}
对应的模板文件 order-confirmation.ftl
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>订单确认 - ${order.orderNo}</title>
<style>
.order-table { width: 100%; border-collapse: collapse; }
.order-table th, .order-table td { padding: 10px; border: 1px solid #ddd; }
</style>
</head>
<body>
<h2>订单确认</h2>
<p>尊敬的 ${customer.name},</p>
<p>感谢您的购买!您的订单详情如下:</p>
<table class="order-table">
<thead>
<tr>
<th>商品名称</th>
<th>单价</th>
<th>数量</th>
<th>小计</th>
</tr>
</thead>
<tbody>
<#list items as item>
<tr>
<td>${item.productName}</td>
<td>¥${item.price?string("0.00")}</td>
<td>${item.quantity}</td>
<td>¥${(item.price * item.quantity)?string("0.00")}</td>
</tr>
</#list>
</tbody>
<tfoot>
<tr>
<td colspan="3" style="text-align: right;"><strong>总计:</strong></td>
<td><strong>¥${totalAmount?string("0.00")}</strong></td>
</tr>
</tfoot>
</table>
<p>订单号:${order.orderNo}</p>
<p>下单时间:${currentDate?string("yyyy-MM-dd HH:mm:ss")}</p>
<p>如有任何问题,请联系客服。</p>
</body>
</html>
# 2.2、创建Configuration实例
freemarker.template.Configuration
类是Freemarker引擎的核心配置类。首先,创建一个Configuration
实例:
import freemarker.template.Configuration;
import freemarker.template.Version;
Configuration cfg = new Configuration(new Version("2.3.31"));
// 设置属性
cfg.setDefaultEncoding(StandardCharsets.UTF_8.name());
Configuration
对象可以设置多个属性来定制 FreeMarker 的行为和特性。其中一些常见的设置包括:
version
: 设置 FreeMarker 的版本号。templateLoader
: 设置模板加载器,用于加载模板文件。objectWrapper
: 设置对象包装器,用于将 Java 对象转换为模板可以识别的模型对象。defaultEncoding
: 设置模板的默认编码。outputEncoding
: 设置模板输出的编码格式。locale
: 设置模板的区域设置。numberFormat
: 设置数字格式化器,用于控制模板中数字的格式化方式。dateFormat
: 设置日期格式化器,用于控制模板中日期的格式化方式。timeFormat
: 设置时间格式化器,用于控制模板中时间的格式化方式。dateTimeFormat
: 设置日期时间格式化器,用于控制模板中日期时间的格式化方式。booleanFormat
: 设置布尔值格式化器,用于控制模板中布尔值的格式化方式。templateExceptionHandler
: 设置模板异常处理器,用于处理模板中的异常。autoFlush
: 设置自动刷新,用于控制 FreeMarker 是否在缓冲区满或模板执行完成后自动刷新输出。wrapUncheckedExceptions
: 设置是否包装非检查异常,用于控制 FreeMarker 是否将非检查异常包装成 TemplateException。logTemplateExceptions
: 设置是否记录模板异常,用于控制 FreeMarker 是否将模板异常记录到日志中。numberFormatFactory
: 设置数字格式化器工厂,用于创建数字格式化器。dateFormatFactory
: 设置日期格式化器工厂,用于创建日期格式化器。timeFormatFactory
: 设置时间格式化器工厂,用于创建时间格式化器。dateTimeFormatFactory
: 设置日期时间格式化器工厂,用于创建日期时间格式化器。booleanFormatFactory
: 设置布尔值格式化器工厂,用于创建布尔值格式化器。
除了上述常见的设置之外,还有一些高级的配置选项可以进一步定制 FreeMarker 的行为和特性,例如缓存设置、模板加载器的缓存设置、模板和数据模型的缓存设置等等。
Configuration
类还有一个setSettings
方法,允许设置Properties
,如果用了这个方法,那么这里面的设置将覆盖上面的设置。
# 2.3、设置模板路径
设置模板文件的加载路径。可以使用setClassForTemplateLoading
方法设置基于类路径的模板加载路径:
cfg.setClassForTemplateLoading(this.getClass(), "/templates");
这样的设置,只适合于模板目录在当前项目中。如果模板目录在引用的jar中,这种方法就不行了,需要自定义TemplateLoader
。以下是一个例子:
import freemarker.cache.URLTemplateLoader;
import org.springframework.util.ClassUtils;
import java.net.URL;
import java.util.Objects;
/**
* 为解决不能读取jar中目录的问题,拓展Freemarker的TemplateLoader
*/
public class ClassloaderTemplateLoader extends URLTemplateLoader {
private final String path;
public ClassloaderTemplateLoader(String path) {
super();
this.path = canonicalizePrefix(path);
}
@Override
protected URL getURL(String name) {
name = path + name;
return Objects.requireNonNull(ClassUtils.getDefaultClassLoader()).getResource(name);
}
}
---
cfg.setTemplateLoader(new ClassloaderTemplateLoader("/templates"));
如果模板来自于字符串,那么需要这么设置:
StringTemplateLoader stringTemplateLoader = new StringTemplateLoader();
cfg.setTemplateLoader(stringTemplateLoader);
# 2.4、生成模板实例
从配置的模板路径中加载模板文件,创建一个模板实例:
import freemarker.template.Template;
Template template = cfg.getTemplate("example.ftl");
对于字符串模板来说,是这样创建的:
String ftlSource = "模板内容...";
String defaultFtlName = "default_" + ftlSource.hashCode();
stringTemplateLoader.putTemplate(defaultFtlName, ftlSource);
try {
Template template = STRING_TEMPLATE_CONFIGURATION.getTemplate(defaultFtlName);
# 2.5、准备数据模型
创建一个数据模型,包含需要在模板中使用的所有变量。通常,数据模型是一个Java Map
对象:
import java.util.HashMap;
import java.util.Map;
Map<String, Object> root = new HashMap<>();
root.put("key", "value");
# 2.6、渲染模板输出
使用模板实例和数据模型渲染输出。将输出写入一个java.io.Writer
对象,例如java.io.StringWriter
或java.io.FileWriter
。
import java.io.StringWriter;
StringWriter out = new StringWriter();
template.process(dataModel, out);
System.out.println(out.toString());
# 2.7、函数与静态方法导入
在Freemarker的dataModel
中,您可以将函数(实现了TemplateMethodModelEx
接口的对象)和工具类的静态方法添加到数据模型中。以下是如何添加函数和静态方法的示例:
# a、添加函数
- 创建一个实现
TemplateMethodModelEx
接口的类。实现exec
方法以定义函数的逻辑。
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModelException;
public class CustomFunction implements TemplateMethodModelEx {
@Override
public Object exec(List arguments) throws TemplateModelException {
// 实现函数逻辑,例如计算两个数的和
int a = ((SimpleNumber) arguments.get(0)).getAsNumber().intValue();
int b = ((SimpleNumber) arguments.get(1)).getAsNumber().intValue();
return a + b;
}
}
- 将函数实例添加到数据模型中。
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("customFunction", new CustomFunction());
- 在模板中使用该函数。
<#assign result = customFunction(5, 3)>
Result: ${result}
# b、导入工具类的静态方法
- 创建一个工具类,包含一个或多个静态方法。
public class Utils {
public static String toUpperCase(String input) {
return input.toUpperCase();
}
public static String toLowerCase(String input) {
return input.toLowerCase();
}
}
- 使用
freemarker.ext.beans.BeansWrapper
将工具类添加到数据模型中。
import freemarker.ext.beans.BeansWrapper;
BeansWrapper beansWrapper = BeansWrapper.getInstance();
TemplateModel statics = beansWrapper.getStaticModels();
dataModel.put("Utils", statics.get("your.package.name.Utils"));
请注意替换your.package.name
为实际工具类所在的包名。
- 在模板中使用静态方法。
Original: ${text}
Upper case: ${Utils.toUpperCase(text)}
Lower case: ${Utils.toLowerCase(text)}
现在,您已经将函数和工具类的静态方法添加到数据模型中,可以在Freemarker模板中直接调用它们。
# 3、Spring Boot集成
在Spring Boot项目中集成Freemarker,您可以在Web控制器中使用模板,并通过模板引擎自动渲染视图。以下是完整的集成方案:
# 3.1、步骤1:添加依赖
Maven项目的pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
Gradle项目的build.gradle
:
implementation 'org.springframework.boot:spring-boot-starter-freemarker'
# 3.2、步骤2:配置application.yml
spring:
freemarker:
# 模板路径
template-loader-path: classpath:/templates/
# 模板后缀
suffix: .ftl
# 编码设置
charset: UTF-8
# 是否缓存模板
cache: false # 开发环境设为false,生产环境设为true
# 是否检查模板路径
check-template-location: true
# 内容类型
content-type: text/html
# 暴露Request属性
expose-request-attributes: true
# 暴露Session属性
expose-session-attributes: true
# 数字格式化
settings:
number_format: 0.##
date_format: yyyy-MM-dd
time_format: HH:mm:ss
datetime_format: yyyy-MM-dd HH:mm:ss
# 经典兼容模式(空值处理)
classic_compatible: true
# 3.3、步骤3:创建完整的电商产品展示示例
Controller层:
@Controller
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/list")
public String listProducts(Model model,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
// 获取产品列表
Page<Product> productPage = productService.findProducts(page, size);
// 准备模型数据
model.addAttribute("products", productPage.getContent());
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", productPage.getTotalPages());
model.addAttribute("totalItems", productPage.getTotalElements());
// 分类和标签
model.addAttribute("categories", productService.getCategories());
model.addAttribute("popularTags", productService.getPopularTags());
return "product/list"; // 对应 templates/product/list.ftl
}
@GetMapping("/{id}")
public String productDetail(@PathVariable Long id, Model model) {
Product product = productService.findById(id);
model.addAttribute("product", product);
model.addAttribute("relatedProducts", productService.findRelated(id));
model.addAttribute("reviews", productService.getReviews(id));
return "product/detail";
}
}
模板文件 templates/product/list.ftl
:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>产品列表</title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<h1>产品列表</h1>
<#-- 分类筛选 -->
<div class="row mb-3">
<div class="col-md-12">
<#list categories as category>
<a href="/products/list?category=${category.id}"
class="btn btn-outline-primary btn-sm">
${category.name}
</a>
</#list>
</div>
</div>
<#-- 产品列表 -->
<div class="row">
<#if products?has_content>
<#list products as product>
<div class="col-md-4 mb-4">
<div class="card">
<img src="${product.imageUrl}" class="card-img-top"
alt="${product.name}">
<div class="card-body">
<h5 class="card-title">${product.name}</h5>
<p class="card-text">
${product.description?truncate(100)}
</p>
<div class="d-flex justify-content-between">
<span class="text-danger h5">
¥${product.price?string("0.00")}
</span>
<#if product.originalPrice??>
<span class="text-muted text-decoration-line-through">
¥${product.originalPrice?string("0.00")}
</span>
</#if>
</div>
<div class="mt-2">
<#-- 标签显示 -->
<#list product.tags as tag>
<span class="badge bg-secondary">${tag}</span>
</#list>
</div>
<a href="/products/${product.id}"
class="btn btn-primary mt-3">查看详情</a>
</div>
</div>
</div>
</#list>
<#else>
<div class="col-12">
<p class="text-center">暂无产品信息</p>
</div>
</#if>
</div>
<#-- 分页 -->
<#if totalPages gt 1>
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
<#-- 上一页 -->
<li class="page-item <#if currentPage == 1>disabled</#if>">
<a class="page-link" href="?page=${currentPage - 1}">上一页</a>
</li>
<#-- 页码 -->
<#list 1..totalPages as pageNum>
<#if (pageNum == 1) || (pageNum == totalPages) ||
(pageNum gte currentPage - 2 && pageNum lte currentPage + 2)>
<li class="page-item <#if pageNum == currentPage>active</#if>">
<a class="page-link" href="?page=${pageNum}">${pageNum}</a>
</li>
<#elseif pageNum == currentPage - 3 || pageNum == currentPage + 3>
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
</#if>
</#list>
<#-- 下一页 -->
<li class="page-item <#if currentPage == totalPages>disabled</#if>">
<a class="page-link" href="?page=${currentPage + 1}">下一页</a>
</li>
</ul>
</nav>
</#if>
<#-- 统计信息 -->
<p class="text-center text-muted">
共 ${totalItems} 个产品,当前第 ${currentPage} / ${totalPages} 页
</p>
</div>
</body>
</html>
# 3.4、步骤4:创建通用布局模板
基础布局 templates/layout/base.ftl
:
<#macro layout title="">
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title!"默认标题"} - 我的网站</title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
<#nested "head">
</head>
<body>
<#include "/layout/header.ftl">
<main class="container my-4">
<#nested "content">
</main>
<#include "/layout/footer.ftl">
<script src="/js/bootstrap.bundle.min.js"></script>
<#nested "scripts">
</body>
</html>
</#macro>
使用布局的页面:
<#import "/layout/base.ftl" as layout>
<@layout.layout title="产品详情">
<#nested "content">
<h1>${product.name}</h1>
<p>${product.description}</p>
<!-- 页面具体内容 -->
</#nested>
<#nested "scripts">
<script>
// 页面特定的JavaScript
</script>
</#nested>
</@layout.layout>
# 3.5、乱码问题
JSP中的乱码问题一般处在<@page>
头部,而在FreeMarker中为避免乱码,需要统一下配置:
#application.properties配置文件中添加
spring.freemarker.settings.defaultEncoding=UTF-8
spring.freemarker.charset=UTF-8
spring.freemarker.charset=UTF-8
# 3.6、JSP模板转移到Freemarker模板
在JSP中可以方便的获取request和application,还可以使用JSTL标签。
在Spring Boot中,提供了平稳的过渡。我们看FreeMarkerView
中的方法:
protected SimpleHash buildTemplateModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) {
AllHttpScopesHashModel fmModel = new AllHttpScopesHashModel(getObjectWrapper(), getServletContext(), request);
fmModel.put(FreemarkerServlet.KEY_JSP_TAGLIBS, this.taglibFactory);
fmModel.put(FreemarkerServlet.KEY_APPLICATION, this.servletContextHashModel);
fmModel.put(FreemarkerServlet.KEY_SESSION, buildSessionModel(request, response));
fmModel.put(FreemarkerServlet.KEY_REQUEST, new HttpRequestHashModel(request, response, getObjectWrapper()));
fmModel.put(FreemarkerServlet.KEY_REQUEST_PARAMETERS, new HttpRequestParametersHashModel(request));
fmModel.putAll(model);
return fmModel;
}
这个方法负责创建一个包含请求和响应相关信息的SimpleHash对象,供Freemarker模板引擎使用。
比如,你可以使用JSP标签库。但是需要注意的是,Freemarker与JSP之间存在差异,因此并非所有的JSP标签都能在Freemarker模板中正常工作。如果可能的话,使用Freemarker的内置指令和宏通常是更好的选择。然而,对于某些特定的JSP标签,您仍然可以在Freemarker模板中使用它们。以下是一个使用JSP标签库的示例:
- 首先,在
src/main/webapp/WEB-INF
目录下创建一个名为freemarker-tags.tld
的文件,用于定义自定义的JSP标签。
<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
version="2.1">
<tlib-version>1.0</tlib-version>
<short-name>custom</short-name>
<uri>http://www.example.com/tags</uri>
<tag>
<name>hello</name>
<tag-class>com.example.tags.HelloTag</tag-class>
<body-content>empty</body-content>
<attribute>
<name>name</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
<type>java.lang.String</type>
</attribute>
</tag>
</taglib>
- 创建一个自定义的JSP标签类,例如
com.example.tags.HelloTag
:
package com.example.tags;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.SimpleTagSupport;
import java.io.IOException;
public class HelloTag extends SimpleTagSupport {
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public void doTag() throws JspException, IOException {
getJspContext().getOut().write("Hello, " + (name != null ? name : "World") + "!");
}
}
- 在Freemarker模板中引入并使用JSP标签库。例如,在
example.ftl
模板文件中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSP Taglib Example</title>
</head>
<body>
<#assign custom=JspTaglibs["http://www.example.com/tags"]>
<@custom.hello name="Freemarker"/>
</body>
</html>
在这个示例中,我们引入了自定义的JSP标签库,并使用hello
标签输出一条问候信息。
也可以直接模板中访问请求属性和请求头信息:
- 在控制器方法中添加请求属性和请求头信息:
@GetMapping("/example")
public ModelAndView example(Model model, HttpServletRequest request, HttpServletResponse response) {
// 添加请求属性
request.setAttribute("attribute", "Request Attribute Value");
// 添加请求头信息
response.addHeader("custom-header", "Custom Header Value");
return new ModelAndView("example");
}
- 在
example.ftl
模板文件中访问请求属性和请求头信息:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HttpRequestHashModel Example</title>
</head>
<body>
<p>Request Attribute: ${Request.attribute["attribute"]}</p>
<p>Custom Header: ${Request.header["custom-header"]}</p>
</body>
</html>
在这个示例中,我们在控制器方法中添加了一个请求属性和一个请求头信息。然后在模板中使用Request.attribute
和Request.header
访问它们。
请注意,HttpRequestHashModel
实例已经通过FreeMarkerView
的buildTemplateModel
方法添加到模型中,因此我们可以直接在模板中访问Request
对象。
如果你还是觉得麻烦,想要直接使用request中的属性,可以设置:
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
这将会把session和request中的属性复制到Freemarker的dataModel中。
如果你的dataModel和request中的取值一致,会出现冲突。可以设置:
spring.freemarker.allow-request-override=true
spring.freemarker.allow-session-override=true
这将允许request中的属性覆盖原有root中的属性。
# 八、Freemarker模板最佳实践
# 1、模板组织与管理
- 目录结构:将模板文件组织在一个统一的目录下,根据功能模块或页面类型创建子目录进行分类。
- 模板命名:使用有意义的名称,遵循一致的命名规范,如
kebab-case
或snake_case
,便于识别和维护。 - 模块化:将公共部分提取为独立的模板文件,通过
<#include>
或<#import>
指令复用,避免重复代码。 - 宏与函数:使用宏和自定义函数封装复杂的逻辑或重复的代码块,提高代码可读性和可维护性。
# 2、代码规范与风格
- 缩进:使用统一的缩进风格,推荐使用2或4个空格进行缩进。
- 注释:在复杂的逻辑或不易理解的代码处添加注释,增加可读性。
- 变量命名:使用有意义的变量名,遵循一致的命名规范,如
camelCase
。 - 指令风格:在指令标签内使用空格分隔关键字、变量和表达式,提高可读性。
- 代码长度:控制每行代码的长度,避免过长的代码,推荐每行不超过80个字符。
# 3、性能优化
- 缓存:为模板配置合适的缓存策略,减少模板解析和渲染的开销。
- 压缩输出:在输出HTML时移除不必要的空格、换行符和注释,减小输出文件的大小,提高传输速度。
- 减少循环和递归:避免过多的循环和递归操作,以减轻服务器的计算负担。
- 预处理数据:尽量在服务器端预处理数据,减少模板中的计算逻辑,提高渲染速度。
- 延迟加载:对于大型的数据集或需要异步获取的数据,使用延迟加载策略,减少页面加载时间。
# 3.1、缓存的使用
在 FreeMarker 中,可以通过配置缓存策略来减少模板解析和渲染的开销,提高性能和响应速度。
以下是一个示例,演示如何将模板缓存到文件系统中。首先,我们需要创建一个用于存储缓存文件的目录,例如 ./cache
:
mkdir cache
然后,在代码中使用以下代码配置缓存策略:
Configuration cfg = new Configuration(new Version("2.3.31"));
cfg.setCacheStorage(new FileCacheStorage(new File("./cache")));
FileTemplateLoader templateLoader = new FileTemplateLoader(new File("./templates"));
cfg.setTemplateLoader(templateLoader);
FileCacheStorage
是一个文件系统缓存存储器,它可以将缓存存储到指定的目录中。在这个示例中,我们将缓存文件存储在 ./cache
目录中,并通过 setCacheStorage
方法将缓存存储器配置到模板加载器中。这样,模板解析和渲染时就会先从缓存中查找模板文件,如果缓存中没有该模板文件,则从文件系统中加载模板文件,并将解析结果存储到缓存中,以便下次使用。
通过这种方式,可以有效地减少模板解析和渲染的开销,提高应用程序的性能和响应速度。当模板文件发生变化时,缓存会自动更新,以保证缓存的一致性和正确性。
缓存文件的格式通常是二进制格式,包含了模板的解析结果、模板的名称和模板的时间戳等信息。缓存文件的名称通常与模板文件的名称和时间戳相关,以保证缓存文件的唯一性和正确性。
FreeMarker 支持多种缓存存储器,包括 ConcurrentMapCacheStorage
、SoftCacheStorage
、MruCacheStorage
、FreemarkerCacheStorage
等等。
FreeMarker 支持多种缓存存储器,它提供了几种不同的 CacheStorage
实现,以满足不同的缓存需求。以下是一些主要的 CacheStorage
实现:
SoftCacheStorage
:此缓存存储使用软引用 (soft references) 实现,这意味着缓存的对象只有在 JVM 内存不足时才会被回收。这是 FreeMarker 的默认缓存存储实现,适用于大多数情况。MruCacheStorage
:最近最少使用(Most Recently Used)策略的缓存存储实现。当缓存达到最大容量时,将删除最近最少使用的条目。这种缓存存储在内存有限的情况下表现良好,但请注意,这不是线程安全的实现。StrongCacheStorage
:此缓存存储使用强引用 (strong references) 实现,这意味着缓存的对象不会被 JVM 回收,除非显式地从缓存中删除。这种缓存存储在内存充足的情况下可以提供更好的性能,但可能导致内存泄漏,因为缓存的对象不会被自动回收。NullCacheStorage
:此缓存存储实际上不缓存任何对象。每次请求模板时,都会重新解析模板文件。这种缓存存储可能在开发环境中有用,因为它允许立即查看模板文件的更改,而无需清除缓存。然而,在生产环境中,它会导致较差的性能,因为模板解析是一个相对耗时的操作。
如果你不设置,那么默认使用SoftCacheStorage
。
# 4、调试与错误处理
- 错误提示:为模板配置友好的错误提示,便于定位问题和进行调试。
- 异常处理:在适当的地方捕获和处理异常,避免程序因异常而中断。
- 日志记录:记录关键操作和异常信息,便于分析问题和监控性能。
- 单元测试:编写单元测试用例,确保模板渲染的正确性和稳定性,及时发现和修复问题。
- 调试工具:使用Freemarker提供的调试工具,如
freemarker.ext.debug
包下的DebugBreak
类,来进行模板调试。在模板中插入${DebugBreak()}
,当程序运行到此处的时候会暂停,进入调试模式。
# 九、常见问题与解决方案
# 1、空值处理问题
问题:访问不存在的变量或null值时抛出异常
The following has evaluated to null or missing:
==> user.name
解决方案:
<#-- 方案1:使用默认值 -->
${user.name!"匿名用户"}
<#-- 方案2:判断是否存在 -->
<#if user?? && user.name??>
${user.name}
</#if>
<#-- 方案3:使用内置函数 -->
${user.name?default("未知")}
<#-- 方案4:配置全局空值处理 -->
<#-- 在Spring Boot中配置 classic_compatible: true -->
# 2、日期格式化问题
问题:日期显示格式不符合预期
解决方案:
<#-- 使用内置格式 -->
${date?date} <#-- 只显示日期 -->
${date?time} <#-- 只显示时间 -->
${date?datetime} <#-- 显示日期时间 -->
<#-- 自定义格式 -->
${date?string("yyyy年MM月dd日")}
${date?string("yyyy-MM-dd HH:mm:ss")}
<#-- 设置全局格式 -->
<#setting date_format="yyyy-MM-dd">
<#setting datetime_format="yyyy-MM-dd HH:mm:ss">
# 3、数字格式化问题
问题:数字显示精度问题或千分位分隔符问题
解决方案:
<#-- 控制小数位数 -->
${price?string("0.00")} <#-- 保留2位小数 -->
${price?string("#.##")} <#-- 最多2位小数 -->
<#-- 去除千分位分隔符 -->
${count?c} <#-- 计算机格式 -->
<#-- 百分比格式 -->
${rate?string.percent} <#-- 显示为百分比 -->
<#-- 货币格式 -->
${amount?string.currency} <#-- 显示为货币 -->
# 4、中文乱码问题
问题:页面显示中文乱码
解决方案:
// Java配置
cfg.setDefaultEncoding("UTF-8");
cfg.setOutputEncoding("UTF-8");
// Spring Boot配置
spring.freemarker.charset=UTF-8
spring.freemarker.settings.default_encoding=UTF-8
spring.freemarker.settings.output_encoding=UTF-8
// 模板文件保存为UTF-8编码
// HTML模板添加meta标签
<meta charset="UTF-8">
# 5、模板找不到问题
问题:TemplateNotFoundException: Template not found
解决方案:
// 检查模板路径配置
cfg.setClassForTemplateLoading(this.getClass(), "/templates");
// 或
cfg.setDirectoryForTemplateLoading(new File("/path/to/templates"));
// Spring Boot检查配置
spring.freemarker.template-loader-path=classpath:/templates/
spring.freemarker.suffix=.ftl
# 6、性能优化问题
问题:模板渲染速度慢
解决方案:
// 1. 启用模板缓存
cfg.setCacheStorage(new SoftCacheStorage());
cfg.setTemplateUpdateDelayMilliseconds(3600000); // 1小时
// 2. Spring Boot生产环境配置
spring.freemarker.cache=true
// 3. 预编译常用模板
Template template = cfg.getTemplate("common.ftl");
// 缓存template对象复用
// 4. 避免在模板中进行复杂计算
// 在Java代码中预处理数据
# 7、循环引用问题
问题:模板之间相互引用导致栈溢出
解决方案:
<#-- 使用import代替include -->
<#import "/common/utils.ftl" as utils>
<#-- 避免循环include -->
<#-- a.ftl include b.ftl, b.ftl不要再include a.ftl -->
<#-- 使用宏定义共享功能 -->
<#macro sharedFunction>
<!-- 共享内容 -->
</#macro>
# 8、特殊字符转义问题
问题:HTML、JavaScript、URL等特殊字符处理
解决方案:
<#-- HTML转义 -->
${content?html}
<#-- JavaScript字符串转义 -->
<script>
var message = "${message?js_string}";
</script>
<#-- URL编码 -->
<a href="/search?q=${keyword?url}">搜索</a>
<#-- JSON转义 -->
{
"name": "${name?json_string}"
}
<#-- 原样输出(不转义) -->
${htmlContent?no_esc}
# 9、调试技巧
开发环境调试配置:
spring:
freemarker:
cache: false # 关闭缓存
settings:
template_exception_handler: html_debug # 错误信息显示在页面
log_template_exceptions: true # 记录模板异常
输出调试信息:
<#-- 输出所有变量 -->
<#list .data_model?keys as key>
${key} = ${.data_model[key]!"null"}
</#list>
<#-- 检查变量类型 -->
${variable?class}
<#-- 条件调试输出 -->
<#if debug!false>
<pre>${object?string}</pre>
</#if>
# 十、总结
在模板引擎领域,Freemarker并不是一枝独秀,它的竞争对手还有Thymeleaf、JSP、Beetl、Velocity等,这些模板引擎也都各具特色。
Freemarker的主要优势在于:
- 成熟稳定:20多年的发展历程,大量企业级应用验证
- 功能强大:丰富的内置函数和指令,满足各种复杂需求
- 性能优秀:模板预编译和缓存机制保证高效渲染
- 生态完善:与主流框架无缝集成,IDE支持良好
- 应用广泛:不限于Web开发,可用于各种文本生成场景
选择模板引擎时应考虑:
- 项目需求和技术栈
- 团队熟悉程度
- 性能要求
- 维护成本
掌握了Freemarker的核心概念和使用方法后,你将能够:
- 快速构建动态Web应用
- 高效生成各类文档和报表
- 实现代码生成器
- 轻松切换到其他模板引擎
模板引擎技术已经非常成熟,Freemarker作为其中的佼佼者,值得每个Java开发者深入学习和掌握。
祝你变得更强!