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: 配置缓存更新检查策略 (关键)

这决定了缓存的“新鲜度”和性能平衡。

  1. 设置模板更新延迟 (setTemplateUpdateDelayMilliseconds):

    • 概念: 定义 FreeMarker 在两次检查模板文件是否更新之间等待的最短时间(毫秒)。在此延迟期间,即使文件已修改,缓存中的 Template 实例也会被重用。
    • 操作:
      • 开发环境 (实时刷新): 设置为 0。每次调用 getTemplate() 都会检查文件时间戳,确保修改立即生效。
        cfg.setTemplateUpdateDelayMilliseconds(0);
        
      • 生产环境 (高性能): 设置一个合理的正数(如 60000 毫秒 = 1分钟)或 -1 (永不检查)。
        // 每分钟检查一次模板更新
        cfg.setTemplateUpdateDelayMilliseconds(60000);
        // 或者,如果模板在部署后永不修改,可设置为 -1 (永不检查)
        // cfg.setTemplateUpdateDelayMilliseconds(-1);
        
    • 注意: 这个设置是全局的,影响 Configuration 管理的所有模板。
  2. 理解检查机制:

    • getTemplate("name.ftl") 被调用时:
      • FreeMarker 首先在缓存中查找 name.ftl
      • 如果找到,检查自上次检查以来是否超过了 templateUpdateDelay
      • 如果未超过延迟,直接返回缓存的 Template
      • 如果超过了延迟,TemplateLoader (如 FileTemplateLoader) 会检查磁盘上文件的最后修改时间。
      • 如果文件时间戳比缓存中记录的新,则重新加载、解析、编译模板,更新缓存并返回新实例。
      • 如果文件未变,则更新缓存的检查时间戳并返回旧实例。

步骤 3: 配置缓存大小与驱逐策略

FreeMarker 使用 ConcurrentMap 作为底层缓存存储,其大小和驱逐策略由 TemplateCache 的构造决定。

  1. 设置缓存大小限制 (setCacheStorage):

    • 概念: 默认缓存 (ConcurrentMap 实现) 会无限增长,可能导致 OutOfMemoryError。可以通过设置 CacheStorage 来限制大小并实现 LRU (Least Recently Used) 等驱逐策略。
    • 操作: 使用 freemarker.cache 包下的 StrongCacheStorageSoftCacheStorage 结合 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);
        
    • 注意: SoftCacheStorage 使用软引用,允许 JVM 在内存不足时回收模板对象,但回收后下次访问需要重新加载。
  2. 设置缓存查找策略 (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: 监控与调试 (可选)

  1. 启用缓存统计:
    • 某些 CacheStorage 实现可能提供统计信息。标准 ConcurrentMap 不直接提供,但可通过 JMX 或自定义包装器实现。
  2. 日志记录:
    • 设置 cfg.setLogTemplateExceptions(true) 可以在异常时记录更多信息(生产环境谨慎使用)。
    • 使用 TemplateExceptionHandler.HTML_DEBUG_HANDLER 可以在页面上显示错误堆栈(仅开发环境)。

3. 常见错误

  1. 模板修改后不生效 (生产环境):
    • 原因: setTemplateUpdateDelayMilliseconds 设置了一个较大的值(如 60000),而你期望立即看到修改。或者设置为 -1 (永不检查)。
    • 解决: 检查 templateUpdateDelay 设置。开发环境设为 0。生产环境需重启应用或等待延迟到期才能看到修改。
  2. OutOfMemoryError (内存溢出):
    • 原因: 缓存无限增长。加载了大量不同的模板(如基于用户输入动态生成模板名),且未设置大小限制。
    • 解决: 实现 CacheStorage 并设置合理的大小限制(如 LRU)或使用 SoftCacheStorage
  3. 缓存击穿 (大量请求同时触发模板加载):
    • 原因: 当一个热门模板缓存失效时,大量并发请求可能同时发现缓存缺失,导致同时尝试加载和编译同一个模板,造成短暂性能下降。
    • 解决: FreeMarker 的 TemplateCache 内部有同步机制(synchronized 块)来防止同一模板被重复加载。确保 Configuration 是单例。如果问题严重,考虑更高级的缓存层(如 Redis 缓存最终 HTML 片段)。
  4. TemplateNotFoundException 但文件存在:
    • 原因: TemplateLoader 的根路径设置错误,导致无法找到文件。或者文件权限问题。
    • 解决: 检查 setClassForTemplateLoadingsetDirectoryForTemplateLoading 的参数是否正确。确认文件路径和权限。
  5. MalformedTemplateNameException:
    • 原因: 传递给 getTemplate() 的名称包含非法字符或路径遍历尝试(如 ../)。
    • 解决: 验证模板名称输入,避免直接使用用户不可信的输入作为模板名。

4. 注意事项

  1. Configuration 单例: Configuration 对象是线程安全的,应在应用启动时创建一个实例,并在整个应用生命周期中共享。不要为每个请求创建新的 Configuration
  2. Template 对象线程安全: Template 实例是线程安全的,可以安全地在多个线程间共享和重复使用。process() 方法是线程安全的。
  3. 缓存键: 缓存键包含模板名和编码。getTemplate("tpl.ftl", "UTF-8")getTemplate("tpl.ftl", "GBK") 会被视为两个不同的缓存项。
  4. 开发 vs 生产: 务必区分开发和生产环境的 templateUpdateDelay 设置。开发环境设为 0 便于调试,生产环境设为正数以获得最佳性能。
  5. 内存监控: 在生产环境中监控 JVM 内存使用情况,特别是如果模板数量巨大或 CacheStorage 无大小限制。
  6. setCacheStorage 的影响: 更改 CacheStorage 会清空现有缓存。应在 Configuration 初始化早期设置。
  7. clearTemplateCache(): 可以调用 cfg.clearTemplateCache() 手动清空所有缓存。这会导致下次访问每个模板时都需要重新加载。可用于强制刷新所有模板(如部署后),但需谨慎,因为会短暂影响性能。

5. 使用技巧

  1. 预热缓存: 在应用启动完成后,可以主动调用 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());
        }
    }
    
  2. 自定义 TemplateLoader: 如果需要从数据库、网络或加密文件加载模板,实现 TemplateLoader 接口。注意在 findTemplateSource() 返回的 Object (通常是 FileTemplateSource 或自定义源对象) 中正确实现 getLastModified() 方法,以便缓存能正确判断更新。
  3. Template 对象重用: 一旦通过 getTemplate() 获得 Template 实例,可以将其存储在类变量或缓存中(如果模板名固定),避免重复调用 getTemplate()(尽管 getTemplate() 本身也利用缓存,但直接持有 Template 实例省去了缓存查找的开销)。注意: 如果 templateUpdateDelay > 0 且文件被修改,直接持有的旧 Template 实例不会自动更新。通常还是推荐每次都调用 getTemplate() 以利用其更新检查机制。
  4. 结合应用级缓存: 对于生成的、变化不频繁的 HTML 内容(如静态页面、产品详情页),可以考虑使用 Redis、Ehcache 等应用级缓存来缓存最终的 HTML 字符串,这比 FreeMarker 模板缓存更进一步,能完全跳过模板处理阶段。

6. 最佳实践与性能优化

  1. 最佳实践:

    • 强制单例 Configuration: 这是基础。
    • 开发/生产差异化配置: 使用配置文件或环境变量区分 templateUpdateDelay
    • 限制缓存大小: 即使模板数量不多,也建议设置一个合理的 LRU 缓存大小(如 100-500),防止意外情况导致内存无限增长。
    • 监控与告警: 监控应用内存、CPU 和模板处理时间。设置告警。
    • 代码审查: 确保模板名称的构造是安全的,避免路径遍历或注入。
    • 文档化: 记录项目的 FreeMarker 配置和缓存策略。
  2. 性能优化:

    • 优化 templateUpdateDelay: 生产环境设置一个足够长的延迟(如 5-15 分钟),最大化缓存命中率。只有在模板确实需要热更新时才缩短它。
    • 使用高效的 CacheStorage: ConcurrentMap (默认) 性能很好。如果需要 LRU,ConcurrentLinkedHashMap 是高效的选择。
    • 减少 getTemplate() 调用开销 (高级): 如果某个模板被极频繁地访问,且你能保证其内容在应用运行期间不会改变(或通过其他机制管理更新),可以考虑在应用启动时加载一次 Template 实例并持有其引用,在处理请求时直接使用 template.process()。但这牺牲了 getTemplate() 的自动更新检查能力,需谨慎评估。
    • 优化模板本身: 简化模板逻辑,避免在模板中进行复杂计算或数据库查询(应通过数据模型预计算好)。使用 #list 指令时注意性能。
    • 异步处理: 对于生成耗时较长的报告等场景,考虑异步生成并缓存结果。
    • JVM 调优: 确保 JVM 有足够的堆内存 (-Xmx),并选择合适的垃圾回收器。

通过遵循这些最佳实践,你可以充分利用 FreeMarker 模板缓存的优势,在保证功能正确性的同时,实现卓越的应用性能。