适用场景:内容管理系统(CMS)、电商详情页、博客生成、SEO 优化页面
目标:将动态数据 + 模板 → 生成.html
静态文件,提升访问速度、降低服务器压力
一、核心概念
1. 什么是静态页面生成?
将 模板(.ftl
) + 数据模型(Java 对象) → 通过 FreeMarker 渲染 → 输出为 .html
文件,部署到 Nginx、CDN 或静态服务器,用户直接访问 HTML,无需后端参与。
2. 为什么使用 FreeMarker 做静态化?
优势 | 说明 |
---|---|
✅ 模板强大 | 支持逻辑、循环、宏、包含等 |
✅ 性能高 | 渲染快,适合批量生成 |
✅ 易集成 | Java 生态,与 Spring、MyBatis 等无缝集成 |
✅ 可维护 | 模板与数据分离,便于前端协作 |
✅ 支持动态数据 | 可从数据库、API 获取数据再生成 |
3. 典型应用场景
- 新闻网站:每发布一篇新闻,生成
news-123.html
- 电商平台:商品详情页生成
product-456.html
- 博客系统:文章发布后生成静态页
- SEO 优化:静态页更利于搜索引擎抓取
二、操作步骤(非常详细)
步骤 1:添加 Maven 依赖
<dependencies>
<!-- FreeMarker 核心 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
<!-- 可选:Spring 集成(如使用 Spring) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.3.31</version>
</dependency>
<!-- 可选:日志 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
</dependencies>
步骤 2:准备 FreeMarker 配置
import freemarker.template.Configuration;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.Version;
public class StaticPageGenerator {
private Configuration cfg;
public StaticPageGenerator() throws IOException {
cfg = new Configuration(Version.VERSION_2_3_32);
// 设置模板目录(classpath 或文件系统)
// 方式一:从 classpath 加载模板
cfg.setClassForTemplateLoading(StaticPageGenerator.class, "/templates");
// 方式二:从文件系统加载
// cfg.setDirectoryForTemplateLoading(new File("/path/to/templates"));
// 设置默认编码
cfg.setDefaultEncoding("UTF-8");
// 异常处理:生产环境使用 RETHROW
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
// 关闭模板缓存(开发期),生产批量生成可开启
cfg.setTemplateUpdateDelayMilliseconds(Long.MAX_VALUE); // 永不检查更新
// 日志设置
cfg.setLogTemplateExceptions(false); // 避免敏感信息泄露
cfg.setWrapUncheckedExceptions(true);
}
}
步骤 3:准备数据模型(Java 对象)
public class Article {
private Long id;
private String title;
private String content;
private String author;
private Date publishTime;
private List<Comment> comments;
// 构造函数、getter/setter 省略
}
public class Comment {
private String nickname;
private String text;
private Date createTime;
// getter/setter
}
步骤 4:创建 FreeMarker 模板(/resources/templates/article.ftl
)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>${article.title} - 博客</title>
<meta name="description" content="${article.content?substring(0, 100)}...">
</head>
<body>
<header>
<h1>${article.title}</h1>
<p>作者:${article.author} | 发布时间:${article.publishTime?datetime}</p>
</header>
<article>
${article.content?no_esc} <!-- 内容可能含 HTML -->
</article>
<section>
<h3>评论(${article.comments?size} 条)</h3>
<#if article.comments??>
<ul>
<#list article.comments as comment>
<li>
<strong>${comment.nickname}</strong>
<small>${comment.createTime?date}</small>
<p>${comment.text}</p>
</li>
</#list>
</ul>
<#else>
<p>暂无评论。</p>
</#if>
</section>
<footer>
<p>© 2025 我的博客. All rights reserved.</p>
</footer>
</body>
</html>
步骤 5:编写静态页生成逻辑
import freemarker.template.Template;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public void generateArticlePage(Article article, String outputDir)
throws IOException, TemplateException {
// 1. 获取模板
Template template = cfg.getTemplate("article.ftl"); // 对应 /templates/article.ftl
// 2. 构建数据模型
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("article", article);
// 3. 创建输出目录
Path outputDirPath = Paths.get(outputDir);
Files.createDirectories(outputDirPath);
// 4. 定义输出文件路径
String fileName = "article-" + article.getId() + ".html";
Path outputPath = outputDirPath.resolve(fileName);
// 5. 渲染并写入文件
try (PrintWriter writer = new PrintWriter(new FileWriter(outputPath.toFile()))) {
template.process(dataModel, writer);
writer.flush();
}
System.out.println("✅ 静态页生成成功: " + outputPath);
}
步骤 6:调用生成方法(示例)
public static void main(String[] args) {
try {
StaticPageGenerator generator = new StaticPageGenerator();
// 模拟从数据库加载文章
Article article = new Article();
article.setId(123L);
article.setTitle("FreeMarker 静态化实战");
article.setContent("<p>本文介绍如何使用 FreeMarker 生成静态页面...</p>");
article.setAuthor("张三");
article.setPublishTime(new Date());
List<Comment> comments = Arrays.asList(
new Comment("李四", "很有用!", new Date()),
new Comment("王五", "已收藏", new Date())
);
article.setComments(comments);
// 生成静态页
generator.generateArticlePage(article, "/var/www/html/articles");
} catch (Exception e) {
e.printStackTrace();
}
}
步骤 7:部署静态页
生成的文件位于:
/var/www/html/articles/article-123.html
可通过 Nginx 直接访问:
server {
listen 80;
root /var/www/html;
index index.html;
location /articles/ {
alias /var/www/html/articles/;
}
}
访问:http://yourdomain.com/articles/article-123.html
三、常见错误与解决方案
错误现象 | 原因 | 解决方案 |
---|---|---|
TemplateNotFoundException |
模板路径错误 | 检查 setClassForTemplateLoading 路径或文件是否存在 |
页面乱码 | 编码未统一 | 确保模板、defaultEncoding 、输出文件均为 UTF-8 |
${...} 未替换 |
数据模型 key 错误 | 检查 dataModel.put("key", value) 与模板中 ${key} 一致 |
?no_esc 未生效 |
内容被转义 | 使用 ?no_esc 或 ?interpret (谨慎) |
生成速度慢 | 大量文件未并行 | 使用多线程批量生成(见性能优化) |
?datetime 格式错误 |
时区问题 | 设置 cfg.setLocale(Locale.CHINA); cfg.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); |
四、注意事项
- 模板路径:
classpath
路径以/
开头,文件系统路径用File
。 - 输出目录权限:确保 Java 进程有写权限。
- 文件覆盖:重复生成会覆盖原文件,注意备份。
- HTML 转义:用户内容用
${text}
(自动转义),可信 HTML 用${text?no_esc}
。 - 资源引用:CSS/JS 图片路径使用相对路径或 CDN 绝对路径。
- SEO 友好:在模板中添加 meta、title、schema 等。
五、使用技巧
1. 批量生成(多线程)
List<Article> articles = articleService.getAllPublished();
ExecutorService executor = Executors.newFixedThreadPool(10);
articles.forEach(article ->
executor.submit(() -> {
try {
generateArticlePage(article, outputDir);
} catch (Exception e) {
log.error("生成文章失败: " + article.getId(), e);
}
})
);
executor.shutdown();
executor.awaitTermination(1, TimeUnit.HOURS);
2. 模板热更新(开发期)
// 开发期:设置较短的更新延迟
cfg.setTemplateUpdateDelayMilliseconds(1000); // 每秒检查一次
3. 使用 #include
复用头部/尾部
<!-- header.ftl -->
<!DOCTYPE html>
<html>
<head><title>${title}</title></head>
<body>
<!-- article.ftl -->
<#include "header.ftl">
<h1>${article.title}</h1>
...
<#include "footer.ftl">
4. 生成目录页(列表页)
public void generateIndexPage(List<Article> articles, String outputDir)
throws IOException, TemplateException {
Template template = cfg.getTemplate("index.ftl");
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("articles", articles);
try (PrintWriter writer = new PrintWriter(new FileWriter(Paths.get(outputDir, "index.html").toFile()))) {
template.process(dataModel, writer);
}
}
5. 自动发布到 CDN(可选)
生成后调用 API 上传到阿里云 OSS、腾讯云 COS 或 CDN 刷新。
六、最佳实践
实践 | 说明 |
---|---|
✅ 模板与代码分离 | 放在 resources/templates/ |
✅ 数据模型扁平化 | 避免深层嵌套 getter |
✅ 使用 ! 处理 null |
${user.name!'游客'} |
✅ 输出路径规范化 | 使用 Paths.get() |
✅ 错误日志记录 | 捕获 TemplateException 并记录 |
✅ 支持增量生成 | 只生成更新的文章 |
✅ 生成前备份旧页(可选) | 防止错误覆盖 |
✅ 使用构建工具集成 | Maven/Gradle 插件生成 |
七、性能优化
优化项 | 说明 |
---|---|
✅ 复用 Configuration |
全局单例,避免重复解析 |
✅ 开启模板缓存 | setTemplateUpdateDelayMilliseconds(Long.MAX_VALUE) |
✅ 多线程批量生成 | 提升吞吐量 |
✅ 压缩 HTML 输出(可选) | 减小文件体积 |
✅ 使用 NIO 写入 | Files.newBufferedWriter |
✅ 避免在模板中计算 | 预计算数据模型 |
✅ CDN 部署 | 提升全球访问速度 |
八、完整流程图
[触发生成] → [加载数据] → [获取模板] → [构建模型] → [渲染 HTML] → [写入文件] → [部署 CDN]
↑ ↑ ↑ ↑
(发布文章) (数据库/Service) (ftl 文件) (Map<String, Object>)
九、总结:静态化检查清单 ✅
项目 | 是否完成 |
---|---|
添加 FreeMarker 依赖 | ☐ |
配置 Configuration |
☐ |
准备模板 .ftl |
☐ |
构建数据模型 | ☐ |
调用 template.process() 写入文件 |
☐ |
处理异常与日志 | ☐ |
设置正确编码(UTF-8) | ☐ |
批量生成使用多线程 | ☐ |
部署到 Web 服务器或 CDN | ☐ |
✅ 一句话总结:
FreeMarker 静态化的核心是 “模板 + 数据 → HTML 文件”,通过 Java 程序批量生成,实现高性能、高可用的静态网站或页面。