适用版本:FreeMarker 2.3.x(主流稳定版本)
目标:提升系统健壮性、避免敏感信息泄露、实现优雅降级与监控
一、核心概念
1. FreeMarker 的异常类型
FreeMarker 在模板渲染过程中可能抛出多种异常,主要分为:
异常类 | 说明 |
---|---|
TemplateException |
模板语法错误、变量未定义、类型不匹配等 |
TemplateNotFoundException |
模板文件未找到 |
ParseException |
模板解析失败(语法错误) |
MalformedTemplateNameException |
模板名格式错误(如路径遍历) |
IOException |
文件读取失败、网络问题等 |
TemplateModelException |
自定义 TemplateModel 实现抛出的异常 |
2. 异常处理的两个层面
- 模板解析阶段(Parse Time):模板首次加载时解析语法。
- 模板执行阶段(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 |
四、注意事项
- 生产环境禁用
DEBUG_HANDLER
:防止源码、路径、变量泄露。 - 不要使用
IGNORE_HANDLER
:掩盖问题,难以排查。 WHITESPACE_HANDLER
仅用于静态生成:不适合 Web 动态渲染。- 自定义处理器中避免复杂逻辑:防止二次异常。
- 日志脱敏:避免记录用户数据或完整模板内容。
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
,结合模板内的 !
/??
操作符,实现健壮、安全、可维护的模板系统。