Apache FreeMarker 快速入门与集成示例
从环境准备到首个模板渲染快速上手 Apache FreeMarker,并给出普通 Java 生成 HTML、邮件与源代码的示例,以及 Spring Boot 与 Servlet 的集成方式与输出结果。
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>):普通插值输出 <b>x</b>,?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等高权限工具,模板应来自可信来源。