1. 核心概念
- 模板缓存 (Template Cache): FreeMarker 的
Configuration
对象内部维护的一个缓存机制,用于存储已解析和编译的Template
对象。当通过getTemplate()
方法请求一个模板时,FreeMarker 会首先检查缓存中是否存在该模板的实例。如果存在且有效,则直接返回缓存的实例;如果不存在或已过期,则从模板源(文件系统、类路径等)加载、解析、编译模板,并将其存入缓存。 Template
对象: 代表一个已解析和编译的 FreeMarker 模板。它是线程安全的,可以被多个线程同时用于处理数据模型并生成输出。创建Template
对象(解析和编译)是相对耗时的操作。- 缓存键 (Cache Key): 通常由模板名称(
templateName
)和可选的编码(encoding
)组成,用于在缓存中唯一标识一个模板实例。 - 缓存更新检查 (Update Check): 决定 FreeMarker 何时检查磁盘上的模板文件是否已被修改,以决定是否需要重新加载和编译模板。
TemplateLookupStrategy
: 控制如何查找和加载模板的策略。它与缓存交互,决定在缓存未命中或需要更新时如何获取模板源。常见的策略有MetaInfFrequentTemplateLookupStrategy
(用于 JAR 包内频繁更新的模板) 和默认的DefaultTemplateLookupStrategy
。TemplateLoader
: 负责从特定位置(如文件系统、类路径)加载模板的原始字节或字符流。它是缓存获取原始模板数据的来源。Configuration
对象: FreeMarker 的核心配置,管理着模板缓存的生命周期、大小、更新策略等。
2. 操作步骤 (非常详细)
以下是配置和管理 FreeMarker 模板缓存的详细步骤:
步骤 1: 创建和配置 Configuration
实例 (基础)
这是所有缓存配置的前提。
import freemarker.cache.*;
import freemarker.template.Configuration;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.Version;
// 1. 创建 Configuration 实例 (指定 FreeMarker 版本)
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32); // 使用实际稳定版本
// 2. 设置模板加载器 (TemplateLoader)
// 例如,从类路径的 /templates 目录加载
cfg.setClassForTemplateLoading(YourClass.class, "/templates");
// 或从文件系统加载
// cfg.setDirectoryForTemplateLoading(new File("/path/to/templates"));
// 3. 设置模板和输出编码 (确保正确,避免因编码问题导致缓存失效或错误)
cfg.setDefaultEncoding("UTF-8");
cfg.setOutputEncoding("UTF-8");
// 4. 设置异常处理 (便于调试缓存相关问题)
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
步骤 2: 配置缓存更新检查策略 (关键)
这决定了缓存的“新鲜度”和性能平衡。
设置模板更新延迟 (
setTemplateUpdateDelayMilliseconds
):- 概念: 定义 FreeMarker 在两次检查模板文件是否更新之间等待的最短时间(毫秒)。在此延迟期间,即使文件已修改,缓存中的
Template
实例也会被重用。 - 操作:
- 开发环境 (实时刷新): 设置为
0
。每次调用getTemplate()
都会检查文件时间戳,确保修改立即生效。cfg.setTemplateUpdateDelayMilliseconds(0);
- 生产环境 (高性能): 设置一个合理的正数(如
60000
毫秒 = 1分钟)或-1
(永不检查)。// 每分钟检查一次模板更新 cfg.setTemplateUpdateDelayMilliseconds(60000); // 或者,如果模板在部署后永不修改,可设置为 -1 (永不检查) // cfg.setTemplateUpdateDelayMilliseconds(-1);
- 开发环境 (实时刷新): 设置为
- 注意: 这个设置是全局的,影响
Configuration
管理的所有模板。
- 概念: 定义 FreeMarker 在两次检查模板文件是否更新之间等待的最短时间(毫秒)。在此延迟期间,即使文件已修改,缓存中的
理解检查机制:
- 当
getTemplate("name.ftl")
被调用时:- FreeMarker 首先在缓存中查找
name.ftl
。 - 如果找到,检查自上次检查以来是否超过了
templateUpdateDelay
。 - 如果未超过延迟,直接返回缓存的
Template
。 - 如果超过了延迟,
TemplateLoader
(如FileTemplateLoader
) 会检查磁盘上文件的最后修改时间。 - 如果文件时间戳比缓存中记录的新,则重新加载、解析、编译模板,更新缓存并返回新实例。
- 如果文件未变,则更新缓存的检查时间戳并返回旧实例。
- FreeMarker 首先在缓存中查找
- 当
步骤 3: 配置缓存大小与驱逐策略
FreeMarker 使用 ConcurrentMap
作为底层缓存存储,其大小和驱逐策略由 TemplateCache
的构造决定。
设置缓存大小限制 (
setCacheStorage
):- 概念: 默认缓存 (
ConcurrentMap
实现) 会无限增长,可能导致OutOfMemoryError
。可以通过设置CacheStorage
来限制大小并实现 LRU (Least Recently Used) 等驱逐策略。 - 操作: 使用
freemarker.cache
包下的StrongCacheStorage
或SoftCacheStorage
结合ConcurrentLinkedHashMap
。- 添加依赖 (如果使用 Maven):
<!-- freemarker 依赖通常已包含 cache 包,但有时需要显式声明 --> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.32</version> </dependency> <!-- 如果需要 LRU,可能需要 concurrentlinkedhashmap-lru --> <dependency> <groupId>com.googlecode.concurrentlinkedhashmap</groupId> <artifactId>concurrentlinkedhashmap-lru</artifactId> <version>1.4.2</version> </dependency>
- 配置 LRU 缓存:
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; import freemarker.cache.ConcurrentMapCacheStorage; // 创建一个最多容纳 200 个模板的 LRU 缓存 ConcurrentLinkedHashMap<String, Object> lruMap = new ConcurrentLinkedHashMap.Builder<String, Object>() .maximumWeightedCapacity(200) .build(); // 将 LRU Map 包装成 CacheStorage CacheStorage lruStorage = new ConcurrentMapCacheStorage(lruMap); // 将自定义的 CacheStorage 设置给 Configuration cfg.setCacheStorage(lruStorage);
- 添加依赖 (如果使用 Maven):
- 注意:
SoftCacheStorage
使用软引用,允许 JVM 在内存不足时回收模板对象,但回收后下次访问需要重新加载。
- 概念: 默认缓存 (
设置缓存查找策略 (
setTemplateLookupStrategy
):- 概念: 控制如何查找模板。默认策略 (
DefaultTemplateLookupStrategy
) 适用于大多数情况。 - 操作 (通常使用默认):
// 显式设置默认策略 (通常不需要) // cfg.setTemplateLookupStrategy(new DefaultTemplateLookupStrategy());
- 特殊情况: 如果模板在 JAR 包内且可能频繁更新(罕见),可考虑
MetaInfFrequentTemplateLookupStrategy
,它会更积极地检查更新。
- 概念: 控制如何查找模板。默认策略 (
步骤 4: 使用 getTemplate()
正确获取模板
这是利用缓存的关键调用。
try {
// 1. 从配置好的 cfg 获取模板
// 名称是相对于 TemplateLoader 设置的根目录的路径
Template template = cfg.getTemplate("user/profile.ftl");
// 2. 准备数据模型
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("user", user);
dataModel.put("now", new Date());
// 3. 处理模板 (process 方法是线程安全的)
Writer out = new OutputStreamWriter(response.getOutputStream(), "UTF-8");
template.process(dataModel, out);
out.flush();
} catch (TemplateNotFoundException e) {
// 模板文件不存在
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Template not found: " + e.getTemplateName());
} catch (MalformedTemplateNameException e) {
// 模板名称格式错误
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid template name");
} catch (ParseException e) {
// 模板语法解析错误
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Template parse error: " + e.getMessage());
} catch (IOException e) {
// IO 错误 (读取模板或写入输出)
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "IO error: " + e.getMessage());
} catch (TemplateException e) {
// 模板处理时的运行时错误 (如调用不存在的方法)
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Template processing error: " + e.getMessage());
}
关键点:
cfg.getTemplate()
调用会自动利用缓存。- 捕获并处理特定异常有助于诊断问题(如模板缺失)。
步骤 5: 监控与调试 (可选)
- 启用缓存统计:
- 某些
CacheStorage
实现可能提供统计信息。标准ConcurrentMap
不直接提供,但可通过 JMX 或自定义包装器实现。
- 某些
- 日志记录:
- 设置
cfg.setLogTemplateExceptions(true)
可以在异常时记录更多信息(生产环境谨慎使用)。 - 使用
TemplateExceptionHandler.HTML_DEBUG_HANDLER
可以在页面上显示错误堆栈(仅开发环境)。
- 设置
3. 常见错误
- 模板修改后不生效 (生产环境):
- 原因:
setTemplateUpdateDelayMilliseconds
设置了一个较大的值(如 60000),而你期望立即看到修改。或者设置为-1
(永不检查)。 - 解决: 检查
templateUpdateDelay
设置。开发环境设为0
。生产环境需重启应用或等待延迟到期才能看到修改。
- 原因:
OutOfMemoryError
(内存溢出):- 原因: 缓存无限增长。加载了大量不同的模板(如基于用户输入动态生成模板名),且未设置大小限制。
- 解决: 实现
CacheStorage
并设置合理的大小限制(如 LRU)或使用SoftCacheStorage
。
- 缓存击穿 (大量请求同时触发模板加载):
- 原因: 当一个热门模板缓存失效时,大量并发请求可能同时发现缓存缺失,导致同时尝试加载和编译同一个模板,造成短暂性能下降。
- 解决: FreeMarker 的
TemplateCache
内部有同步机制(synchronized
块)来防止同一模板被重复加载。确保Configuration
是单例。如果问题严重,考虑更高级的缓存层(如 Redis 缓存最终 HTML 片段)。
TemplateNotFoundException
但文件存在:- 原因:
TemplateLoader
的根路径设置错误,导致无法找到文件。或者文件权限问题。 - 解决: 检查
setClassForTemplateLoading
或setDirectoryForTemplateLoading
的参数是否正确。确认文件路径和权限。
- 原因:
MalformedTemplateNameException
:- 原因: 传递给
getTemplate()
的名称包含非法字符或路径遍历尝试(如../
)。 - 解决: 验证模板名称输入,避免直接使用用户不可信的输入作为模板名。
- 原因: 传递给
4. 注意事项
Configuration
单例:Configuration
对象是线程安全的,应在应用启动时创建一个实例,并在整个应用生命周期中共享。不要为每个请求创建新的Configuration
。Template
对象线程安全:Template
实例是线程安全的,可以安全地在多个线程间共享和重复使用。process()
方法是线程安全的。- 缓存键: 缓存键包含模板名和编码。
getTemplate("tpl.ftl", "UTF-8")
和getTemplate("tpl.ftl", "GBK")
会被视为两个不同的缓存项。 - 开发 vs 生产: 务必区分开发和生产环境的
templateUpdateDelay
设置。开发环境设为0
便于调试,生产环境设为正数以获得最佳性能。 - 内存监控: 在生产环境中监控 JVM 内存使用情况,特别是如果模板数量巨大或
CacheStorage
无大小限制。 setCacheStorage
的影响: 更改CacheStorage
会清空现有缓存。应在Configuration
初始化早期设置。clearTemplateCache()
: 可以调用cfg.clearTemplateCache()
手动清空所有缓存。这会导致下次访问每个模板时都需要重新加载。可用于强制刷新所有模板(如部署后),但需谨慎,因为会短暂影响性能。
5. 使用技巧
- 预热缓存: 在应用启动完成后,可以主动调用
getTemplate()
加载一些关键或常用的模板,使它们提前进入缓存,避免第一个用户请求时经历编译延迟。// 应用启动后执行 String[] criticalTemplates = {"home.ftl", "login.ftl", "error.ftl"}; for (String tpl : criticalTemplates) { try { cfg.getTemplate(tpl); System.out.println("Preloaded template: " + tpl); } catch (Exception e) { System.err.println("Failed to preload " + tpl + ": " + e.getMessage()); } }
- 自定义
TemplateLoader
: 如果需要从数据库、网络或加密文件加载模板,实现TemplateLoader
接口。注意在findTemplateSource()
返回的Object
(通常是FileTemplateSource
或自定义源对象) 中正确实现getLastModified()
方法,以便缓存能正确判断更新。 Template
对象重用: 一旦通过getTemplate()
获得Template
实例,可以将其存储在类变量或缓存中(如果模板名固定),避免重复调用getTemplate()
(尽管getTemplate()
本身也利用缓存,但直接持有Template
实例省去了缓存查找的开销)。注意: 如果templateUpdateDelay
> 0 且文件被修改,直接持有的旧Template
实例不会自动更新。通常还是推荐每次都调用getTemplate()
以利用其更新检查机制。- 结合应用级缓存: 对于生成的、变化不频繁的 HTML 内容(如静态页面、产品详情页),可以考虑使用 Redis、Ehcache 等应用级缓存来缓存最终的 HTML 字符串,这比 FreeMarker 模板缓存更进一步,能完全跳过模板处理阶段。
6. 最佳实践与性能优化
最佳实践:
- 强制单例
Configuration
: 这是基础。 - 开发/生产差异化配置: 使用配置文件或环境变量区分
templateUpdateDelay
。 - 限制缓存大小: 即使模板数量不多,也建议设置一个合理的 LRU 缓存大小(如 100-500),防止意外情况导致内存无限增长。
- 监控与告警: 监控应用内存、CPU 和模板处理时间。设置告警。
- 代码审查: 确保模板名称的构造是安全的,避免路径遍历或注入。
- 文档化: 记录项目的 FreeMarker 配置和缓存策略。
- 强制单例
性能优化:
- 优化
templateUpdateDelay
: 生产环境设置一个足够长的延迟(如 5-15 分钟),最大化缓存命中率。只有在模板确实需要热更新时才缩短它。 - 使用高效的
CacheStorage
:ConcurrentMap
(默认) 性能很好。如果需要 LRU,ConcurrentLinkedHashMap
是高效的选择。 - 减少
getTemplate()
调用开销 (高级): 如果某个模板被极频繁地访问,且你能保证其内容在应用运行期间不会改变(或通过其他机制管理更新),可以考虑在应用启动时加载一次Template
实例并持有其引用,在处理请求时直接使用template.process()
。但这牺牲了getTemplate()
的自动更新检查能力,需谨慎评估。 - 优化模板本身: 简化模板逻辑,避免在模板中进行复杂计算或数据库查询(应通过数据模型预计算好)。使用
#list
指令时注意性能。 - 异步处理: 对于生成耗时较长的报告等场景,考虑异步生成并缓存结果。
- JVM 调优: 确保 JVM 有足够的堆内存 (
-Xmx
),并选择合适的垃圾回收器。
- 优化
通过遵循这些最佳实践,你可以充分利用 FreeMarker 模板缓存的优势,在保证功能正确性的同时,实现卓越的应用性能。