适用版本:FreeMarker 2.3.x(主流稳定版本)
目标:提升系统健壮性、避免敏感信息泄露、实现优雅降级与监控


一、核心概念

1. FreeMarker 的异常类型

FreeMarker 在模板渲染过程中可能抛出多种异常,主要分为:

异常类 说明
TemplateException 模板语法错误、变量未定义、类型不匹配等
TemplateNotFoundException 模板文件未找到
ParseException 模板解析失败(语法错误)
MalformedTemplateNameException 模板名格式错误(如路径遍历)
IOException 文件读取失败、网络问题等
TemplateModelException 自定义 TemplateModel 实现抛出的异常

2. 异常处理的两个层面

  1. 模板解析阶段(Parse Time):模板首次加载时解析语法。
  2. 模板执行阶段(Render Time)template.process() 时访问变量、执行逻辑。

3. TemplateExceptionHandler 的作用

这是 FreeMarker 的核心异常处理机制,用于控制当模板执行中发生异常时的行为。它是一个接口,可自定义处理逻辑。


二、操作步骤(非常详细)

步骤 1:引入依赖(Maven)

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.32</version>
</dependency>

步骤 2:配置 Configuration 并设置异常处理器

import freemarker.template.Configuration;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.Version;

Configuration cfg = new Configuration(Version.VERSION_2_3_32);
cfg.setDefaultEncoding("UTF-8");

✅ 设置异常处理器(关键步骤)

// 方式一:使用内置处理器(推荐)
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
// 其他选项:
// - DEBUG_HANDLER: 开发期使用,输出详细信息
// - HTML_DEBUG_HANDLER: 输出 HTML 格式调试信息
// - IGNORE_HANDLER: 忽略异常(不推荐)
// - WHITESPACE_HANDLER: 用空格替换异常部分

⚠️ 生产环境强烈建议使用 RETHROW_HANDLER,由上层框架统一处理。


步骤 3:使用 RETHROW_HANDLER 并捕获异常(生产推荐)

try {
    Template template = cfg.getTemplate("user-profile.ftl");
    StringWriter out = new StringWriter();
    
    Map<String, Object> dataModel = new HashMap<>();
    dataModel.put("user", userService.findById(1));
    
    // 执行渲染(异常在此抛出)
    template.process(dataModel, out);
    
    return out.toString();
    
} catch (TemplateNotFoundException e) {
    log.error("模板未找到: {}", e.getTemplateName(), e);
    return handleTemplateNotFound(e.getTemplateName());
    
} catch (MalformedTemplateNameException e) {
    log.error("非法模板名: {}", e.getMessage(), e);
    return handleInvalidTemplateName(e);
    
} catch (ParseException e) {
    log.error("模板语法错误: {}, 行: {}, 列: {}", 
              e.getTemplateName(), e.getLineNumber(), e.getColumnNumber(), e);
    return handleParseError(e);
    
} catch (TemplateException e) {
    log.error("模板执行错误: {}, 行: {}, 消息: {}", 
              e.getTemplateName(), e.getLineNumber(), e.getMessage(), e);
    return handleTemplateError(e);
    
} catch (IOException e) {
    log.error("IO错误(读取模板失败)", e);
    return handleIOError(e);
    
} catch (Exception e) {
    log.error("未知错误", e);
    return handleUnexpectedError(e);
}

步骤 4:自定义 TemplateExceptionHandler(高级用法)

import freemarker.template.Template;
import freemarker.template.TemplateException;

public class CustomExceptionHandler implements TemplateExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(CustomExceptionHandler.class);

    @Override
    public void handleTemplateException(TemplateException te, Environment env, Writer out)
            throws TemplateException {

        String templateName = env.getTemplate().getName();
        int line = te.getLineNumber();
        String message = te.getMessage();

        // 日志记录(脱敏)
        log.warn("FreeMarker 模板异常 [模板={}][行={}]: {}", 
                 templateName, line, sanitizeMessage(message));

        try {
            // 向输出流写入降级内容(如占位符)
            out.write("<span class='error'>内容加载失败</span>");
            
            // 可选:继续渲染后续内容
            if (env.getTemplateExceptionHandler() == this) {
                // 不再递归处理
            }
            
        } catch (IOException e) {
            throw new TemplateException("无法写入错误占位符", env, e);
        }
    }

    private String sanitizeMessage(String msg) {
        // 防止敏感路径泄露
        return msg.replaceAll("/[^/ ]+", "/***");
    }
}

注册自定义处理器:

cfg.setTemplateExceptionHandler(new CustomExceptionHandler());

✅ 适用场景:CMS、用户可编辑模板、需要优雅降级的系统。


步骤 5:在模板中使用 ??! 避免空值异常

<!-- 使用默认值 -->
${user.nickname!'游客'}

<!-- 条件判断 -->
<#if user.email??>
  <p>Email: ${user.email}</p>
</#if>

<!-- 避免调用 null 对象的方法 -->
${(user.profile.bio?default(''))?html}

步骤 6:启用异常包装与上下文增强(可选)

// 包装异常,保留原始堆栈
cfg.setWrapUncheckedExceptions(true);

// 记录异常时是否包含模板片段
cfg.setLogTemplateExceptions(true); // 默认 true

// 但生产环境建议关闭,防止信息泄露
cfg.setLogTemplateExceptions(false);

三、常见错误与解决方案

错误现象 原因 解决方案
页面显示堆栈信息 使用了 DEBUG_HANDLER 改用 RETHROW 或自定义处理器
模板未找到无提示 IGNORE_HANDLER 改为 RETHROW 并捕获
user.name 报错 user 为 null 使用 user.name!'N/A'
?size on null 集合未初始化 使用 items?size?default(0)
循环中异常中断渲染 未处理 使用自定义处理器降级
敏感路径泄露 日志打印模板路径 日志脱敏或关闭 logTemplateExceptions

四、注意事项

  1. 生产环境禁用 DEBUG_HANDLER:防止源码、路径、变量泄露。
  2. 不要使用 IGNORE_HANDLER:掩盖问题,难以排查。
  3. WHITESPACE_HANDLER 仅用于静态生成:不适合 Web 动态渲染。
  4. 自定义处理器中避免复杂逻辑:防止二次异常。
  5. 日志脱敏:避免记录用户数据或完整模板内容。
  6. null 值优先使用 !?? 处理,而非依赖异常机制。

五、使用技巧

1. 使用 #attempt / #recover 捕获局部异常(FreeMarker 2.3.30+)

<#attempt>
  <p>用户名: ${user.name}</p>
  <p>积分: ${user.profile.score}</p>
<#recover>
  <p class="error">用户数据加载失败</p>
</#attempt>

✅ 优势:局部降级,不影响整体渲染。
⚠️ 注意:#attempt 语法较新,需确认版本支持。


2. 异常上下文增强(Environment)

在自定义函数或宏中获取环境信息:

public Object exec(List arguments) throws TemplateModelException {
    Environment env = Environment.getCurrentEnvironment();
    String currentTemplate = env.getTemplate().getName();
    TemplateModel currentNamespace = env.getCurrentNamespace();
    
    log.debug("函数执行于模板: {}", currentTemplate);
    // ...
}

3. 监控与告警

// 统计异常次数(如使用 Micrometer)
Counter templateErrorCounter = Counter.builder("freemarker.errors")
    .tag("type", "template_exception")
    .register(registry);

// 在异常处理器中增加
templateErrorCounter.increment();

六、最佳实践

实践 说明
✅ 生产使用 RETHROW_HANDLER 由 Spring/Servlet 统一处理
✅ 开发使用 HTML_DEBUG_HANDLER 便于调试
✅ 自定义处理器用于降级 CMS、用户模板场景
✅ 模板中广泛使用 !?? 防御性编程
✅ 记录日志但脱敏 安全审计
✅ 启用 wrapUncheckedExceptions 保留原始异常
✅ 禁用 logTemplateExceptions(生产) 防止信息泄露
✅ 使用 #attempt 局部捕获 提升容错能力

七、性能优化

优化项 说明
❌ 避免在异常处理器中做耗时操作 如远程调用、大文件写入
✅ 使用异步日志 如 Logback AsyncAppender
✅ 降级内容简单 减少 out.write() 开销
✅ 缓存 Template 对象 减少解析异常发生概率

八、完整异常处理流程图

模板渲染开始
     ↓
加载模板 → 模板不存在? → TemplateNotFoundException
     ↓
解析模板 → 语法错误? → ParseException
     ↓
执行渲染 → 变量访问异常? → TemplateException
     ↓
自定义逻辑 → TemplateModelException
     ↓
[异常处理器介入]
     ↓
→ RETHROW → 上层捕获 → 返回错误页
→ CUSTOM → 写入降级内容 → 继续渲染
→ IGNORE → 静默跳过(不推荐)

九、总结:异常处理检查清单 ✅

项目 是否完成
生产环境使用 RETHROW_HANDLER
开发环境使用 HTML_DEBUG_HANDLER
捕获并处理 TemplateException
模板中使用 !?? 防 null
自定义处理器用于降级(如需)
日志脱敏,不泄露路径
启用 wrapUncheckedExceptions
禁用 logTemplateExceptions(生产)
使用 #attempt 局部捕获(可选)

一句话总结
FreeMarker 异常处理的核心是 “分层处理、生产脱敏、开发调试、模板防御” —— 通过合理配置 TemplateExceptionHandler,结合模板内的 !/?? 操作符,实现健壮、安全、可维护的模板系统。