Java 模板引擎 FreeMarker 入门教程:语法、内建函数与常用案例

文章目录

    • [1. 引言](#1. 引言)
    • [2. 环境准备与快速开始](#2. 环境准备与快速开始)
      • [2.1 maven依赖配置](#2.1 maven依赖配置)
      • [2.2 第一个 FreeMarker 程序](#2.2 第一个 FreeMarker 程序)
    • [3. FreeMarker 核心语法详解](#3. FreeMarker 核心语法详解)
    • [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 安全注意事项

  1. 防止模板注入:不要允许用户上传或编辑模板文件。
  2. 转义用户输入 :在输出用户提供的内容时,始终使用 ?html?js_string 进行转义。
  3. 限制数据模型:避免将敏感数据放入数据模型。
  4. 验证模板来源:确保模板文件来自可信路径。

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 -->
相关推荐
冷雨夜中漫步10 小时前
SQLite 深度解析:在 Java/Spring 中的使用与H2、Derby对比
java·spring·sqlite
wengqidaifeng10 小时前
C++从菜鸟到强手:2.类和对象(上)—— 从结构体到类的跨越
java·开发语言·c++
自律懒人10 小时前
2026年AI编程工具横评:Trae、Cursor、Claude Code、Copilot X,同一需求谁更强?
java·copilot·ai编程
夕除11 小时前
spring boot 13
java·mysql·spring
marlondu11 小时前
ScopedValue:Java 21 引入的结构化作用域值
java
risc12345611 小时前
DocumentsWriterDeleteQueue
java·开发语言
日月云棠11 小时前
12 Dubbo 2.7 服务发布全流程源码解析
java·后端
用户2986985301411 小时前
告别手动复制:Java 拆分 Word 文档的两种实用方案
java·后端
ujainu小11 小时前
CANN hixl:大模型 PD 分离场景的零拷贝通信库
android·java·缓存