安全地使用 FreeMarker 渲染 JSON 数据:防止 Null 异常和变量缺失
在 Java 开发中,FreeMarker 是一个强大的模板引擎,常用于动态渲染 HTML、邮件模板、文档等。而在一些自动化系统或低代码平台中,FreeMarker 也被用于渲染结构化的 JSON 文本。
但在实际应用中,一个常见的痛点是:
当数据模型中存在 null 或缺失字段时,模板渲染可能抛出异常或输出不可预期内容。
本文将介绍一种通用且安全的 FreeMarker 使用方式,适用于对用户友好、对异常容忍度高的系统,如:问答机器人、配置系统、低代码平台等。
问题场景
来看一个典型模板:
Hello, ${user.name!'Unknown'}!
Your age is: ${user.age!'Not provided'}
Your address: ${user.address.city!'No city'}, ${user.address.country!'No country'}
Skills: ${(user.skills)!'[java,python,sql]'}
Nested data: ${nestedData.key1!'Default value'}
这个模板假设了 user
和 nestedData
对象的存在。
一旦 user
为 null 或 user.address.country
缺失,就可能报错:
FreeMarker template error: The following has evaluated to null or missing: ==> user.address.country
安全设计原则
为了解决这个问题,我们采用以下策略:
启用 classic-compatible 模式
允许访问不存在的变量时返回 null 而非抛异常。自定义 ObjectWrapper
对 null 值和集合做统一封装,避免默认行为带来的空指针问题。模板中使用
!
运算符
使用 FreeMarker 的默认值语法${var!'default'}
,让模板更容错。
代码示例:安全封装 FreeMarker
以下是一套完整的示例代码,支持:
null 安全访问
集合渲染为字符串(如
"Java, Python"
)兼容嵌套属性访问
安全 fallback 机制(即便
user
整个为 null 也不会出错)
@Slf4j
public class Example {
public static void main(String[] args) {
String templateStr =
"Hello, ${user.name!'Unknown'}!\n" +
"Your age is: ${user.age!'Not provided'}\n" +
"Your address: ${user.address.city!'No city'}, ${user.address.country!'No country'}\n" +
"Skills: ${(user.skills)!'[java,python,sql]'}\n" +
"Nested data: ${nestedData.key1!'Default value'}";
case1(templateStr); // 无数据模型,全部 fallback
case2(templateStr); // 部分字段缺失
}
public static void case1(String templateStr) {
System.out.println("case1 ==================================");
System.out.println(format(templateStr, null));
}
public static void case2(String templateStr) {
Map<String, Object> data = new HashMap<>();
Map<String, Object> user = new HashMap<>();
user.put("name", "Alice");
user.put("age", null);
Map<String, String> address = new HashMap<>();
address.put("city", "Beijing");
user.put("address", address);
user.put("skills", Arrays.asList("Java", "Python", "SQL"));
data.put("user", user);
System.out.println("case2 ==================================");
System.out.println(format(templateStr, data));
}
public static String format(String templateStr, Map<String, Object> dataModel) {
if (templateStr == null) return "";
try {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setDefaultEncoding("UTF-8");
// 关键配置1:兼容模式,缺失字段返回null
cfg.setClassicCompatible(true);
// 关键配置2:设置安全封装器
cfg.setObjectWrapper(new SafeObjectWrapper(Configuration.VERSION_2_3_32));
Template template = new Template("dynamicTemplate", new StringReader(templateStr), cfg);
Map<String, Object> safeData = dataModel != null ? dataModel : new HashMap<>();
StringWriter out = new StringWriter();
template.process(safeData, out);
return out.toString();
} catch (Exception e) {
log.error("模板渲染失败: {}", ExceptionUtil.stacktraceToString(e));
return "模板渲染错误: " + e.getMessage();
}
}
/**
* 安全封装器:处理null、集合等
*/
private static class SafeObjectWrapper extends DefaultObjectWrapper {
public SafeObjectWrapper(Version version) {
super(version);
setExposeFields(true);
}
@Override
public TemplateModel wrap(Object obj) throws TemplateModelException {
if (obj == null) return null;
// 特殊处理集合:渲染为 "a, b, c"
if (obj instanceof Collection) {
Collection<?> col = (Collection<?>) obj;
return new SimpleScalar(col.stream()
.map(o -> o != null ? o.toString() : "")
.collect(Collectors.joining(", ")));
}
return super.wrap(obj);
}
@Override
protected TemplateModel handleUnknownType(Object obj) throws TemplateModelException {
return obj != null ? new SimpleScalar(obj.toString()) : null;
}
}
}
输出示例
case1(数据为 null)
Hello, Unknown!
Your age is: Not provided
Your address: No city, No country
Skills: [java,python,sql]
Nested data: Default value
case2(部分字段存在)
Hello, Alice!
Your age is: Not provided
Your address: Beijing, No country
Skills: Java, Python, SQL
Nested data: Default value
小结:模板安全的三板斧
✅ 最终效果是:即便数据模型缺失字段、值为 null、字段嵌套多层,模板都能稳定输出,不影响用户体验。
推荐用法场景
渲染用户侧可视化配置模板(如自定义欢迎语)
数据导出系统(如导出 JSON 或 YAML 文件)
邮件通知、日志模版等系统
AI Prompt 渲染(例如向大模型发送结构化模板)
Freemark依赖
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
评论