Apache FreeMarker 快速入门与集成示例

从环境准备到首个模板渲染快速上手 Apache FreeMarker,并给出普通 Java 生成 HTML、邮件与源代码的示例,以及 Spring Boot 与 Servlet 的集成方式与输出结果。

Apache FreeMarkerApache FreeMarker2026/10/12

Apache FreeMarker 快速入门与集成示例

本文先带你完成 Apache FreeMarker 的环境准备与第一个模板渲染(真实可复现),再给出普通 Java、Spring Boot、Servlet 三类集成场景的示例代码与输出。示例基于 FreeMarker 2.3.35,配合 JDK 8/11/17 与 Maven 3.5+。

一、快速入门

1.1 环境准备

  • JDK 8/11/17 任一,java -version 可正常输出版本。
  • Maven 3.5 及以上,mvn -v 可用。
  • 全局操作账号 root(示例口令 q1w2e3r4)。
  • 创建工程并加入依赖:
mkdir -p /root/freemarker-demo/src/main/java
mkdir -p /root/freemarker-demo/templates

/root/freemarker-demo/pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.quzuo.demo</groupId>
    <artifactId>freemarker-demo</artifactId>
    <version>1.0.0</version>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.35</version>
        </dependency>
    </dependencies>
</project>

1.2 第一个渲染示例

模板 templates/hello.ftl:

Hello ${user}!
<#if items?size gt 0>
Items (${items?size}):
<#list items as it>
- ${it?upper_case}
</#list>
<#else>
No items.
</#if>
<#macro footer name>
-- generated for ${name} --
</#macro>
<@footer name=user?upper_case/>
Price: ${price}
Active: ${active}
Now: ${now?date} / ${now?datetime}
Version: ${version}

Java 代码 src/main/java/Demo.java:

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;

import java.io.File;
import java.io.FileWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class Demo {
    public static void main(String[] args) throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_35);
        cfg.setDirectoryForTemplateLoading(new File("templates"));
        cfg.setDefaultEncoding("UTF-8");
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        cfg.setNumberFormat("0.00");
        cfg.setBooleanFormat("yes,no");
        cfg.setDateFormat("yyyy-MM-dd");
        cfg.setDateTimeFormat("yyyy-MM-dd HH:mm:ss");

        Map<String, Object> data = new HashMap<String, Object>();
        data.put("user", "quzuo");
        data.put("items", Arrays.asList("alpha", "beta", "gamma"));
        data.put("price", 1234.5);
        data.put("active", Boolean.TRUE);
        data.put("now", new Date());
        data.put("version", Configuration.getVersion());

        Template tpl = cfg.getTemplate("hello.ftl");

        StringWriter sw = new StringWriter();
        tpl.process(data, sw);
        System.out.println("=== render to string ===");
        System.out.print(sw.toString());

        Writer fw = new FileWriter("out.html");
        try {
            tpl.process(data, fw);
        } finally {
            fw.close();
        }
        System.out.println("=== render to file: out.html ===");
        System.out.println("bytes written: " + new File("out.html").length());
    }
}

编译运行:

cd /root/freemarker-demo
mvn -B dependency:build-classpath -Dmdep.outputFile=cp.txt
mvn -B clean compile
java -Xms256m -Xmx256m -cp "target/classes:$(cat cp.txt)" Demo

真实回显:

=== FreeMarker version: 2.3.35 ===
=== render to string ===
Hello quzuo!
Items (3.00):
- ALPHA
- BETA
- GAMMA
-- generated for QUZUO --
Price: 1234.50
Active: yes
Now: 2026-10-11 / 2026-10-11 23:42:26
Version: 2.3.35
=== render to file: out.html ===
bytes written: 157

1.3 结果解读

  • 数据模型:Map<String,Object> 作为根对象,键名即模板中的变量名;process(data, writer) 将模板与数据合并。
  • 插值:${user} 输出 quzuo;${version} 来自 Configuration.getVersion(),验证运行时库版本为 2.3.35。
  • 指令:<#if> 判定列表非空,<#list> 逐项输出,宏 <#macro>/<@...> 复用片段。
  • 内建函数:?upper_case 转大写(alpha→ALPHA);?date/?datetime 按 dateFormat/dateTimeFormat 格式化时间。
  • 格式化:setNumberFormat("0.00") 使 1234.5→1234.50(连 ${items?size} 也变成 3.00);setBooleanFormat("yes,no") 使 true→yes。
  • 输出目标:既可直接写入 StringWriter 得到字符串,也可写入文件 out.html(157 字节);两者内容一致。

常见坑:boolean_format 不要设为 true,false,否则 FreeMarker 视为未设置并抛出转换异常,请用 yes,no 或模板内 ${bool?c}。

二、普通 Java 集成示例

2.1 生成 HTML 页面

配置 HTML 输出格式以启用自动转义:

import freemarker.core.HTMLOutputFormat;

Configuration cfg = new Configuration(Configuration.VERSION_2_3_35);
cfg.setDirectoryForTemplateLoading(new File("templates"));
cfg.setDefaultEncoding("UTF-8");
cfg.setOutputFormat(HTMLOutputFormat.INSTANCE);
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);

模板 templates/page.ftl:

<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>${title}</title></head>
<body>
  <h1>${title}</h1>
  <ul>
  <#list rows as r>
    <li>${r.name} - ${r.score}</li>
  </#list>
  </ul>
  <p>raw: ${remark?no_esc}</p>
</body>
</html>

实测自动转义效果(数据含 <b>x</b>):普通插值输出 &lt;b&gt;x&lt;/b&gt;,?no_esc 处原样输出 <b>x</b>。

2.2 生成邮件正文

邮件正文同样可通过模板生成,配置为纯文本输出:

Configuration cfg = new Configuration(Configuration.VERSION_2_3_35);
cfg.setClassLoaderForTemplateLoading(MailDemo.class.getClassLoader(), "templates");
cfg.setDefaultEncoding("UTF-8");
cfg.setOutputFormat(freemarker.core.PlainTextOutputFormat.INSTANCE);

模板 templates/mail.ftl:

尊敬的 ${name}:

您本次的订单号 ${orderNo} 已完成,金额 ${amount} 元。
<#if coupon??>优惠券已抵扣 ${coupon} 元。</#if>

-- ${company}

使用 StringWriter 得到正文后交给 JavaMail 发送(此处只演示渲染):

Map<String, Object> model = new HashMap<String, Object>();
model.put("name", "张三");
model.put("orderNo", "QZ20261012001");
model.put("amount", "199.00");
model.put("coupon", "20.00");
model.put("company", "quzuo");

StringWriter sw = new StringWriter();
cfg.getTemplate("mail.ftl").process(model, sw);
String body = sw.toString();

2.3 生成源代码/配置

利用 <#list> 与条件渲染批量产出重复代码结构:

package ${pkg};

public class ${cls} {
<#list fields as f>
    private ${f.type} ${f.name};
</#list>
<#list fields as f>
    public ${f.type} get${f.name?cap_first}() { return ${f.name}; }
    public void set${f.name?cap_first}(${f.type} ${f.name}) { this.${f.name} = ${f.name}; }
</#list>
}
model.put("pkg", "com.quzuo.model");
model.put("cls", "User");
model.put("fields", Arrays.asList(
        new HashMap<String, Object>() {{ put("type", "String"); put("name", "name"); }},
        new HashMap<String, Object>() {{ put("type", "int"); put("name", "age"); }}));

运行后即可得到完整 Java 源码字符串,写入 .java 文件即可。

三、Spring Boot 集成示例

3.1 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

该 starter 已传递引入 org.freemarker:freemarker,无需重复声明。

3.2 配置

在 application.properties 中配置:

spring.freemarker.enabled=true
spring.freemarker.template-loader-path=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=UTF-8
spring.freemarker.content-type=text/html;charset=UTF-8
spring.freemarker.cache=true
spring.freemarker.settings.number_format=0.00
spring.freemarker.settings.boolean_format=yes,no
spring.freemarker.settings.datetime_format=yyyy-MM-dd HH:mm:ss

3.3 Controller 与模板

src/main/resources/templates/user.ftl:

<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>用户信息</title></head>
<body>
  <p>欢迎,${user.name?upper_case}!</p>
  <ul>
  <#list user.roles as role>
    <li>${role}</li>
  </#list>
  </ul>
</body></html>

Controller:

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Arrays;

@Controller
public class UserController {

    @GetMapping("/user")
    public String user(Model model) {
        model.addAttribute("user", new java.util.HashMap<String, Object>() {{
            put("name", "quzuo");
            put("roles", Arrays.asList("admin", "viewer"));
        }});
        return "user";   // 解析为 templates/user.ftl
    }
}

访问 /user 时将返回渲染后的 HTML。Spring 会自动创建并复用 FreeMarkerConfigurer/Configuration,无需手工实例化。

四、Servlet 集成示例

4.1 web.xml 注册 FreemarkerServlet

<servlet>
    <servlet-name>freemarker</servlet-name>
    <servlet-class>freemarker.ext.servlet.FreemarkerServlet</servlet-class>
    <init-param>
        <param-name>TemplatePath</param-name>
        <param-value>/WEB-INF/templates</param-value>
    </init-param>
    <init-param>
        <param-name>default_encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>number_format</param-name>
        <param-value>0.00</param-value>
    </init-param>
    <init-param>
        <param-name>boolean_format</param-name>
        <param-value>yes,no</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>freemarker</servlet-name>
    <url-pattern>*.ftl</url-pattern>
</servlet-mapping>

4.2 Servlet 代码

import freemarker.ext.servlet.FreemarkerServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.util.HashMap;
import java.util.Map;

public class UserServlet extends FreemarkerServlet {
    @Override
    protected boolean preTemplateProcess(HttpServletRequest request,
                                         HttpServletResponse response,
                                         freemarker.template.Template template,
                                         freemarker.template.TemplateModel dataModel) {
        Map<String, Object> user = new HashMap<String, Object>();
        user.put("name", "quzuo");
        request.setAttribute("user", user);
        return true;   // 继续渲染
    }
}

FreemarkerServlet 会把请求属性(request、session、Request 等)自动放入数据模型,模板可直接访问。

4.3 模板 WEB-INF/templates/user.ftl

<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>用户</title></head>
<body><p>Hello ${user.name}</p></body></html>

说明:Spring Boot 与 Servlet 两节为集成代码形态示例,实际运行需完整的 Web 运行环境;其中涉及的 Configuration 行为与本文第一节实测一致。

五、最佳实践与注意事项

  • 单例复用:Configuration 应作为应用级单例,避免每次渲染都新建。
  • 显式编码:始终 setDefaultEncoding("UTF-8"),模板文件以 UTF-8 保存。
  • 异常策略:生产环境使用 RETHROW_HANDLER 并关闭 setLogTemplateExceptions,由上层统一处理。
  • 自动转义:HTML 输出统一设置 setOutputFormat(HTMLOutputFormat.INSTANCE),遏制 XSS。
  • 避免全局格式化副作用:number_format、boolean_format 是全局的,可用 ${num?string("0.00")} 在模板内按需格式化以降低耦合。
  • 堆内存:模板渲染内存占用低,JVM 堆建议 256–512MB,如 -Xms256m -Xmx256m;高并发可结合 setTemplateUpdateDelayMilliseconds 减少模板重载检查开销。
  • 安全:不要向模板暴露 freemarker.template.utility.Execute、ObjectConstructor 等高权限工具,模板应来自可信来源。