轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

    • 核心

    • 并发

    • 经验

    • JVM

    • 企业应用

      • Freemarker模板实战指南
        • 一、Freemarker模板简介
          • 1、为什么选择Freemarker?
          • 2、典型应用场景
          • 2.1、Web开发领域
          • 2.2、代码生成领域
          • 2.3、配置文件生成
        • 二、Freemarker模板的基本语法
          • 1、模板文件结构
          • 2、数据模型
          • 3、注释
          • 4、变量
        • 三、表达式
          • 1、基本表达式
          • 1.1、变量表达式
          • 1.2、字符串表达式
          • 1.3、数字表达式
          • 1.4、布尔表达式
          • 1.5、空值(默认值)表达式
          • 2、算术表达式
          • 3、比较表达式
          • 4、逻辑表达式
          • 5、范围表达式
          • 6、切片
          • 7、方法调用
          • 8、缺省值和缺失值判断
          • 9、其他
        • 四、Freemarker模板指令
          • 1、条件指令
          • 1.1、if指令
          • 1.2、elseif和else指令
          • 1.3、switch指令
          • 2、循环指令
          • 2.1、list指令
          • 3、包含与宏指令
          • 3.1、include指令
          • 3.2、import指令
          • 3.3、macro指令
          • 4、变量与函数指令
          • 4.1、assign指令
          • 4.2、local指令
          • 4.3、global指令
          • 4.4、function指令
          • 5、模板设置指令
          • 5.1、setting指令
          • 6、输出控制指令
          • 6.1、escape指令
          • 6.2、autoesc指令
          • 6.3、noparse指令
          • 6.4、compress指令
          • 6.5、t/lt/rt指令
          • 7、调试指令
          • 7.1、stop指令
          • 7.2、attempt指令和recover指令
          • 8、自定义指令
        • 五、内置函数
          • 1、常用内置函数示例
          • 2、字符串函数
          • 3、数字函数
          • 4、日期函数
          • 5、布尔函数
          • 6、序列函数
          • 7、哈希表函数
          • 8、循环变量函数
          • 9、XML节点函数
          • 10、switch函数
          • 11、专家函数
        • 六、特殊变量引用
        • 七、Freemarker模板与Java集成
          • 1、Java环境配置
          • 2、Java代码实现
          • 2.1、完整示例:生成HTML邮件
          • 2.2、创建Configuration实例
          • 2.3、设置模板路径
          • 2.4、生成模板实例
          • 2.5、准备数据模型
          • 2.6、渲染模板输出
          • 2.7、函数与静态方法导入
          • a、添加函数
          • b、导入工具类的静态方法
          • 3、Spring Boot集成
          • 3.1、步骤1:添加依赖
          • 3.2、步骤2:配置application.yml
          • 3.3、步骤3:创建完整的电商产品展示示例
          • 3.4、步骤4:创建通用布局模板
          • 3.5、乱码问题
          • 3.6、JSP模板转移到Freemarker模板
        • 八、Freemarker模板最佳实践
          • 1、模板组织与管理
          • 2、代码规范与风格
          • 3、性能优化
          • 3.1、缓存的使用
          • 4、调试与错误处理
        • 九、常见问题与解决方案
          • 1、空值处理问题
          • 2、日期格式化问题
          • 3、数字格式化问题
          • 4、中文乱码问题
          • 5、模板找不到问题
          • 6、性能优化问题
          • 7、循环引用问题
          • 8、特殊字符转义问题
          • 9、调试技巧
        • 十、总结
      • Servlet与JSP指南
      • Java日志系统
      • Java JSON处理
      • Java XML处理
      • Java对象池技术
      • Java程序的单元测试(Junit)
      • Thymeleaf官方文档(中文版)
      • Mockito应用
      • Java中的空安全
  • Spring

  • 其他语言

  • 工具

  • 后端
  • Java
  • 企业应用
轩辕李
2022-09-20
目录

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、变量表达式

  1. 简单变量
${userName}
${totalAmount}

在模板中,使用${}包裹变量名以获取变量的值。

  1. 哈希表(对象属性访问)
${user.name}
${user.address.street}
${order.customer.email}

哈希表中的元素可以通过.操作符访问,支持多级属性访问。

  1. 序列(数组/列表访问)
${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、算术表达式

  1. 加法
${a + b}
  1. 减法
${a - b}
  1. 乘法
${a * b}
  1. 除法
${a / b}
  1. 取余
${a % b}
  1. 求幂
${a ^ b}

# 3、比较表达式

  1. 等于
${a == b}
  1. 不等于
${a != b}
  1. 大于
${a > b}
  1. 小于
${a < b}
  1. 大于等于
${a >= b}
  1. 小于等于
${a <= b}

# 4、逻辑表达式

  1. 与(AND)
${a && b}
  1. 或(OR)
${a || b}
  1. 非(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>

输出:

  &amp;
  ...
  &amp;

# 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参数是一个字符串,可以包含以下字符:

  1. r:要查找的子字符串是一个正则表达式。
  2. i:在匹配时忽略大小写。
  3. m:启用多行模式。^ 和 $ 可以匹配每一行的开头和结尾。
  4. s:启用点号(.)匹配任意字符,包括换行符。
  5. c:允许在正则表达式中使用空格和注释。
  6. 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、添加函数
  1. 创建一个实现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;
    }
}
  1. 将函数实例添加到数据模型中。
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("customFunction", new CustomFunction());
  1. 在模板中使用该函数。
<#assign result = customFunction(5, 3)>
Result: ${result}
# b、导入工具类的静态方法
  1. 创建一个工具类,包含一个或多个静态方法。
public class Utils {

    public static String toUpperCase(String input) {
        return input.toUpperCase();
    }

    public static String toLowerCase(String input) {
        return input.toLowerCase();
    }
}
  1. 使用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为实际工具类所在的包名。

  1. 在模板中使用静态方法。
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标签库的示例:

  1. 首先,在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>
  1. 创建一个自定义的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") + "!");
    }
}
  1. 在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标签输出一条问候信息。


也可以直接模板中访问请求属性和请求头信息:

  1. 在控制器方法中添加请求属性和请求头信息:
@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");
}
  1. 在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、模板组织与管理

  1. 目录结构:将模板文件组织在一个统一的目录下,根据功能模块或页面类型创建子目录进行分类。
  2. 模板命名:使用有意义的名称,遵循一致的命名规范,如kebab-case或snake_case,便于识别和维护。
  3. 模块化:将公共部分提取为独立的模板文件,通过<#include>或<#import>指令复用,避免重复代码。
  4. 宏与函数:使用宏和自定义函数封装复杂的逻辑或重复的代码块,提高代码可读性和可维护性。

# 2、代码规范与风格

  1. 缩进:使用统一的缩进风格,推荐使用2或4个空格进行缩进。
  2. 注释:在复杂的逻辑或不易理解的代码处添加注释,增加可读性。
  3. 变量命名:使用有意义的变量名,遵循一致的命名规范,如camelCase。
  4. 指令风格:在指令标签内使用空格分隔关键字、变量和表达式,提高可读性。
  5. 代码长度:控制每行代码的长度,避免过长的代码,推荐每行不超过80个字符。

# 3、性能优化

  1. 缓存:为模板配置合适的缓存策略,减少模板解析和渲染的开销。
  2. 压缩输出:在输出HTML时移除不必要的空格、换行符和注释,减小输出文件的大小,提高传输速度。
  3. 减少循环和递归:避免过多的循环和递归操作,以减轻服务器的计算负担。
  4. 预处理数据:尽量在服务器端预处理数据,减少模板中的计算逻辑,提高渲染速度。
  5. 延迟加载:对于大型的数据集或需要异步获取的数据,使用延迟加载策略,减少页面加载时间。

# 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 实现:

  1. SoftCacheStorage:此缓存存储使用软引用 (soft references) 实现,这意味着缓存的对象只有在 JVM 内存不足时才会被回收。这是 FreeMarker 的默认缓存存储实现,适用于大多数情况。

  2. MruCacheStorage:最近最少使用(Most Recently Used)策略的缓存存储实现。当缓存达到最大容量时,将删除最近最少使用的条目。这种缓存存储在内存有限的情况下表现良好,但请注意,这不是线程安全的实现。

  3. StrongCacheStorage:此缓存存储使用强引用 (strong references) 实现,这意味着缓存的对象不会被 JVM 回收,除非显式地从缓存中删除。这种缓存存储在内存充足的情况下可以提供更好的性能,但可能导致内存泄漏,因为缓存的对象不会被自动回收。

  4. NullCacheStorage:此缓存存储实际上不缓存任何对象。每次请求模板时,都会重新解析模板文件。这种缓存存储可能在开发环境中有用,因为它允许立即查看模板文件的更改,而无需清除缓存。然而,在生产环境中,它会导致较差的性能,因为模板解析是一个相对耗时的操作。

如果你不设置,那么默认使用SoftCacheStorage。

# 4、调试与错误处理

  1. 错误提示:为模板配置友好的错误提示,便于定位问题和进行调试。
  2. 异常处理:在适当的地方捕获和处理异常,避免程序因异常而中断。
  3. 日志记录:记录关键操作和异常信息,便于分析问题和监控性能。
  4. 单元测试:编写单元测试用例,确保模板渲染的正确性和稳定性,及时发现和修复问题。
  5. 调试工具:使用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开发者深入学习和掌握。

祝你变得更强!

编辑 (opens new window)
#Freemarker
上次更新: 2025/08/15
从Hotspot到GraalVM
Servlet与JSP指南

← 从Hotspot到GraalVM Servlet与JSP指南→

最近更新
01
AI时代的编程心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code实战之供应商切换工具
08-18
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式