文章目录
-
- [1. 引言](#1. 引言)
- [2. 环境准备与快速开始](#2. 环境准备与快速开始)
-
- [2.1 maven依赖配置](#2.1 maven依赖配置)
- [2.2 第一个 FreeMarker 程序](#2.2 第一个 FreeMarker 程序)
- [3. FreeMarker 核心语法详解](#3. FreeMarker 核心语法详解)
-
- [3.1 插值(Interpolation)](#3.1 插值(Interpolation))
- [3.2 指令(Directives)](#3.2 指令(Directives))
- [3.3 变量赋值与作用域](#3.3 变量赋值与作用域)
- [3.4 空值处理](#3.4 空值处理)
- [4. 常用内建函数(Built-ins)详解](#4. 常用内建函数(Built-ins)详解)
-
- [4.1 字符串处理](#4.1 字符串处理)
- [4.2 数字与日期格式化](#4.2 数字与日期格式化)
- [4.3 集合与序列操作](#4.3 集合与序列操作)
- [4.4 布尔值转换与默认值](#4.4 布尔值转换与默认值)
- [4.5 其他实用内建函数](#4.5 其他实用内建函数)
- [5. 常用案例汇总](#5. 常用案例汇总)
-
- [5.1 生成动态HTML邮件模板](#5.1 生成动态HTML邮件模板)
- [5.2 代码生成器模板](#5.2 代码生成器模板)
- [5.3 报表数据展示](#5.3 报表数据展示)
- [5.4 配置文件动态生成](#5.4 配置文件动态生成)
- [6. 高级特性与最佳实践](#6. 高级特性与最佳实践)
-
- [6.1 自定义指令](#6.1 自定义指令)
- [6.2 模板缓存与性能优化](#6.2 模板缓存与性能优化)
- [6.3 安全注意事项](#6.3 安全注意事项)
- [6.4 调试技巧](#6.4 调试技巧)
1. 引言
FreeMarker 是一款功能强大、灵活的 Java 模板引擎,广泛应用于 Web 开发、代码生成、邮件模板、报表输出等场景。它采用"数据模型 + 模板 = 输出"的设计哲学,将业务逻辑与展示逻辑彻底分离,使得前端页面或文档模板更加清晰、易于维护。
2. 环境准备与快速开始
2.1 maven依赖配置
在 Maven 项目的 pom.xml 中添加 FreeMarker 依赖:
xml
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
Maven 的核心作用:
-
依赖管理, 只需声明坐标,Maven 自动从中央仓库下载 jar 包及其所有子依赖;
-
构建自动化, 一条命令完成编译、测试、打包、部署等完整生命周期;
-
项目标准化, 统一了目录结构(如 src/main/java),让任何开发者都能快速上手;
-
多模块管理,轻松管理由多个子项目组成的大型工程;
配置参数说明:
-
groupId ,组织/公司标识
含义:类似于 Java 的包名,通常使用反向域名来保证全球唯一性;
-
artifactId,模块名称
含义:该组织下某个具体项目的唯一名称,freemarker 就是模板引擎这个核心库的名字;
-
version,版本号,2.3.32 是一个稳定的发布版本。
当你保存 pom.xml 后,Maven 会在后台自动执行以下流程:
- 查本地仓库:先看本地电脑上有没有 freemarker-2.3.32.jar
- 查远程仓库:如果没有,就去 Maven Central(中央仓库)下载
- 解析传递依赖:检查 FreeMarker 自身是否还依赖其他库,一并下载
- 加入 Classpath:将所有相关 jar 包自动添加到项目的编译和运行路径中
IDE 集成:IntelliJ IDEA / Eclipse 都内置了 Maven,修改 pom.xml 后点击"Reload/刷新"即可生效。
版本查询:不确定用什么版本时,可以去 MVN Repository搜索,选择使用量最高的稳定版(避免 SNAPSHOT 快照版)。
理解 scope:遇到 test 这样的配置,它表示该依赖只在测试阶段有效,不会被打进最终的生产包里,这是 Maven 精细化管理的体现。
2.2 第一个 FreeMarker 程序
以下是一个最简单的示例,演示如何将数据模型与模板结合生成文本输出。
java
import freemarker.template.Configuration; // FreeMarker 的核心配置类
import freemarker.template.Template; // 模板对象,代表一个 .ftl 文件
import freemarker.template.TemplateException; // 模板处理异常(如语法错误、变量缺失)
import java.io.IOException; // IO 异常(如模板文件找不到)
import java.io.StringWriter; // 字符输出流,将结果写入内存字符串
import java.util.HashMap; // 数据容器
import java.util.Map; // 数据模型接口
public class Demo {// 公共类
//公有静态方法main
public static void main(String[] args) throws IOException, TemplateException {
// 1. 创建配置对象,创建成本高,一般使用单例且线程安全
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
// 设置模板.ftl文件的 加载路径
// 基于当前类所在classpath位置从根目录开始查找
cfg.setClassForTemplateLoading(Demo.class, "/");
// 2. 准备数据模型 字典哈希
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("user", "张三");// 存储key-value 键值对
dataModel.put("message", "欢迎学习 FreeMarker!");
// 数据模型就是传递给模板的上下文变量。
// 在模板中可以通过 ${user} 和 ${message} 直接访问。
// FreeMarker 的数据模型支持多种类型:
// Map:最常用的键值对结构
// JavaBean / POJO:直接用对象,模板中通过属性名访问
// List / 数组:用于 <#list arr as item> 循环遍历
// 3. 查找并加载模板,后续同名走缓存
Template template = cfg.getTemplate("hello.ftl");
// 若找不到模板,则抛出IOException
// 4. 合并模板与数据,输出结果
StringWriter out = new StringWriter(); // 写入内存中
// FileWriter 写入文件中
// 将数据模型写入模板中,渲染的字符串保存在out内存中
template.process(dataModel, out);
// 输出
System.out.println(out.toString());
}
}
模板文件 hello.ftl 内容如下:
freemarker
<!DOCTYPE html>
<html>
<head>
<title>欢迎页</title>
</head>
<body>
<h1>你好,${user}!</h1>
<p>${message}</p>
<p>当前时间:${.now?string("yyyy-MM-dd HH:mm:ss")}</p>
</body>
</html>
运行程序,你将得到渲染后的 HTML 内容。
3. FreeMarker 核心语法详解
3.1 插值(Interpolation)
插值用于在模板中输出数据模型的值,语法为 ${expression}。
freemarker
<!-- 输出简单变量 -->
<p>用户名:${userName}</p>
<!-- 输出对象属性 -->
<p>用户年龄:${user.age}</p>
<!-- 输出列表元素 -->
<p>第一个爱好:${hobbies[0]}</p>
<!-- 输出Map值 -->
<p>城市:${address["city"]}</p>
3.2 指令(Directives)
指令是 FreeMarker 的控制结构,以 <# 开始,以 > 结束(或使用 [# 和 ])。
3.2.1 条件判断 <#if>
freemarker
<#if user.role == "admin">
<p>欢迎管理员 ${user.name}!</p>
<#elseif user.role == "vip">
<p>尊贵的 VIP 用户,您好!</p>
<#else>
<p>普通用户,欢迎您。</p>
</#if>
3.2.2 循环遍历 <#list>
freemarker
<ul>
<#list products as product>
<li>${product_index + 1}. ${product.name} - 价格:¥${product.price}</li>
</#list>
</ul>
循环内可用的内置变量:
product_index:当前迭代的索引(从0开始)product_has_next:布尔值,是否有下一个元素
3.2.3 宏定义与调用 <#macro>
宏类似于函数,用于定义可重用的模板片段。
freemarker
<#macro greet name>
<p>你好,${name}!今天是 ${.now?date}。</p>
</#macro>
<!-- 调用宏 -->
<@greet name="李四"/>
<@greet name="王五"/>
3.2.4 包含其他模板 <#include>
freemarker
<#include "header.ftl">
<h1>主要内容区域</h1>
<#include "footer.ftl">
3.3 变量赋值与作用域
freemarker
<#-- 定义全局变量(在整个模板中有效) -->
<#assign globalVar = "我是全局变量">
<#-- 定义局部变量(仅在当前指令块内有效) -->
<#list 1..3 as i>
<#local localVar = "局部值 ${i}">
${localVar}
</#list>
3.4 空值处理
FreeMarker 对空值非常敏感,直接输出 null 会抛出异常。以下是安全的处理方式:
freemarker
<#-- 使用默认值 -->
<p>用户昵称:${user.nickname!"未设置"}</p>
<#-- 使用空值判断 -->
<#if user.bio??>
<p>个人简介:${user.bio}</p>
<#else>
<p>该用户暂无简介。</p>
</#if>
<#-- 链式空值处理 -->
<p>所在部门:${user.department.leader.name!"未知"}</p>
4. 常用内建函数(Built-ins)详解
内建函数类似于对象的方法,通过 ? 符号调用,用于对变量进行格式化、转换、判断等操作。
4.1 字符串处理
freemarker
<#assign str = " Hello, FreeMarker! ">
原始字符串:"${str}"
去除首尾空格:"${str?trim}"
转换为大写:"${str?upper_case}"
转换为小写:"${str?lower_case}"
字符串长度:${str?length}
是否包含子串:${str?contains("Free")?c}
截取前5个字符:"${str?substring(0, 5)}"
分割字符串:<#list str?split(",") as part>${part} </#list>
4.2 数字与日期格式化
freemarker
<#assign num = 1234567.8912>
<#assign now = .now>
数字格式:
${num} ->
作为货币:${num?string.currency}
保留两位小数:${num?string["0.00"]}
千分位显示:${num?string["#,###"]}
日期时间格式:
默认格式:${now?string}
自定义格式:${now?string("yyyy年MM月dd日 HH:mm:ss")}
仅日期部分:${now?date}
仅时间部分:${now?time}
时间戳(毫秒):${now?long}
4.3 集合与序列操作
freemarker
<#assign list = ["苹果", "香蕉", "橙子", "葡萄"]>
<#assign map = {"name": "张三", "age": 25, "city": "北京"}>
列表操作:
第一个元素:${list?first}
最后一个元素:${list?last}
反转列表:<#list list?reverse as item>${item} </#list>
连接为字符串:${list?join("、")}
排序:<#list list?sort as item>${item} </#list>
Map操作:
所有键:<#list map?keys as key>${key} </#list>
所有值:<#list map?values as value>${value} </#list>
键值对数量:${map?size}
4.4 布尔值转换与默认值
freemarker
<#assign flag = true>
布尔值转字符串:${flag?string("是", "否")}
布尔值转字符:${flag?c} <#-- 输出 'true' 或 'false' -->
<#-- 使用内建函数提供默认值 -->
${undefinedVar!} <#-- 空字符串 -->
${undefinedVar!"默认值"} <#-- 指定默认值 -->
${undefinedVar?default("默认值")} <#-- 另一种写法 -->
4.5 其他实用内建函数
freemarker
<#-- 转义HTML -->
<#assign htmlContent = "<script>alert('xss')</script>">
未转义:${htmlContent}
已转义:${htmlContent?html}
<#-- 转换JSON -->
<#assign data = {"name": "John", "age": 30}>
JSON字符串:${data?json_string}
<#-- 判断类型 -->
${num?is_number?c}
${str?is_string?c}
${list?is_sequence?c}
${map?is_hash?c}
5. 常用案例汇总
5.1 生成动态HTML邮件模板
freemarker
<#-- email_template.ftl -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>订单确认通知</title>
</head>
<body style="font-family: Arial, sans-serif;">
<h2>尊敬的 ${order.customerName},您的订单已确认!</h2>
<table border="1" cellpadding="8" style="border-collapse: collapse;">
<tr>
<th>商品名称</th>
<th>单价</th>
<th>数量</th>
<th>小计</th>
</tr>
<#list order.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>
<tr>
<td colspan="3" align="right"><strong>订单总额:</strong></td>
<td><strong>¥${order.totalAmount?string["0.00"]}</strong></td>
</tr>
</table>
<p>预计送达时间:${order.estimatedDelivery?string("yyyy-MM-dd HH:mm")}</p>
<p>如有疑问,请联系客服:400-xxx-xxxx</p>
<#include "footer_signature.ftl">
</body>
</html>
5.2 代码生成器模板
freemarker
<#-- entity.java.ftl -->
package ${packageName};
import lombok.Data;
import javax.persistence.*;
import java.util.Date;
/**
* ${classComment!}
*
* @author ${author!"System"}
* @since ${.now?string("yyyy-MM-dd")}
*/
@Data
@Entity
@Table(name = "${tableName}")
public class ${className} {
<#list fields as field>
<#if field.comment??>
/**
* ${field.comment}
*/
</#if>
<#if field.id>
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
</#if>
<#if field.columnName??>
@Column(name = "${field.columnName}"<#if field.length gt 0>, length = ${field.length}</#if>)
</#if>
private ${field.type} ${field.name};
</#list>
}
5.3 报表数据展示
freemarker
<#-- report.ftl -->
<h1>${reportTitle} - ${.now?string("yyyy年MM月dd日")}</h1>
<#list reportData as category>
<h2>${category.categoryName} (总计:${category.totalCount} 条)</h2>
<table class="table table-striped">
<thead>
<tr>
<th>序号</th>
<th>项目名称</th>
<th>负责人</th>
<th>完成进度</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<#list category.items as item>
<tr>
<td>${item_index + 1}</td>
<td>${item.projectName}</td>
<td>${item.owner}</td>
<td>
<div class="progress">
<div class="progress-bar" style="width: ${item.progress}%;">
${item.progress}%
</div>
</div>
</td>
<td>
<#if item.status == "completed">
<span class="badge bg-success">已完成</span>
<#elseif item.status == "delayed">
<span class="badge bg-danger">已延期</span>
<#else>
<span class="badge bg-warning">进行中</span>
</#if>
</td>
</tr>
</#list>
</tbody>
</table>
</#list>
<#-- 汇总统计 -->
<div class="summary">
<p>项目总数:${reportData?size}</p>
<p>已完成项目:<#assign completed = reportData?flat_map(c -> c.items)?filter(item -> item.status == "completed")>${completed?size}</p>
<p>总体完成率:${(completed?size / reportData?flat_map(c -> c.items)?size * 100)?string["0.0"]}%</p>
</div>
5.4 配置文件动态生成
freemarker
<#-- application.yml.ftl -->
<#if profile == "dev">
server:
port: 8080
servlet:
context-path: /${appName}
spring:
datasource:
url: jdbc:mysql://localhost:3306/${dbName}_dev
username: dev_user
password: ${devPassword}
redis:
host: localhost
port: 6379
logging:
level:
com.${groupId}.${artifactId}: DEBUG
<#elseif profile == "prod">
server:
port: 80
servlet:
context-path: /
spring:
datasource:
url: jdbc:mysql://${dbHost}:3306/${dbName}_prod
username: ${prodUser}
password: ${prodPassword}
redis:
host: ${redisHost}
port: 6379
password: ${redisPassword}
logging:
level:
com.${groupId}.${artifactId}: INFO
root: WARN
</#if>
<#-- 公共配置 -->
app:
version: ${version!"1.0.0"}
upload:
max-size: 10MB
allowed-types: ${allowedTypes?join(",")}
6. 高级特性与最佳实践
6.1 自定义指令
当内置指令无法满足需求时,可以创建自定义指令(Java 实现)。
java
// 实现 TemplateDirectiveModel 接口
public class UpperCaseDirective implements TemplateDirectiveModel {
@Override
public void execute(Environment env, Map params, TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
// 检查参数
TemplateModel textModel = (TemplateModel) params.get("text");
if (textModel == null) {
throw new TemplateModelException("参数 'text' 不能为空");
}
// 获取文本并转换为大写
String text = ((SimpleScalar) textModel).getAsString();
String upperText = text.toUpperCase();
// 输出结果
Writer out = env.getOut();
out.write(upperText);
}
}
// 在配置中注册自定义指令
cfg.setSharedVariable("upper", new UpperCaseDirective());
模板中使用:
freemarker
<@upper text="hello world"/> <#-- 输出:HELLO WORLD -->
6.2 模板缓存与性能优化
java
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
// 启用模板缓存(生产环境推荐)
cfg.setCacheStorage(new StrongCacheStorage());
cfg.setTemplateUpdateDelayMilliseconds(3600000); // 1小时更新一次
// 设置模板加载器
cfg.setTemplateLoader(new ClassTemplateLoader(YourClass.class, "/templates"));
// 设置异常处理器
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
cfg.setLogTemplateExceptions(false);
6.3 安全注意事项
- 防止模板注入:不要允许用户上传或编辑模板文件。
- 转义用户输入 :在输出用户提供的内容时,始终使用
?html或?js_string进行转义。 - 限制数据模型:避免将敏感数据放入数据模型。
- 验证模板来源:确保模板文件来自可信路径。
6.4 调试技巧
freemarker
<#-- 在模板中输出调试信息 -->
<#assign debugInfo = {
"user": user,
"currentTime": .now,
"templateName": .current_template_name
}>
<#-- 输出到控制台(开发环境) -->
${debugInfo?json_string}
<#-- 使用 ?eval 进行表达式求值(谨慎使用) -->
<#assign x = 10>
<#assign expression = "x * 2 + 5">
计算结果:${expression?eval} <#-- 输出 25 -->