Freemarker模板调用Java方法返回HTML字符串的实现及转义问题
我用Freemarker(FTL)模板生成HTML页面,传入了一个包含元素列表的对象。因为要实现复杂逻辑——按key渲染表格行,无对应元素时渲染空行——如果用Freemarker的if、list指令实现,模板代码会冗余复杂,还得重复写逻辑。
所以我打算用Java代码实现表格行的渲染逻辑,写了renderRow方法生成HTML行字符串,示例代码如下:
... public String renderRow(String key){ StringBuilder sb = new StringBuilder(); if(elements.contains(key)){ for(var element: elements.get(key)){ sb.append("<tr>"); sb.append("<td>"); sb.append(element.getNumber()); // 其他HTML拼接逻辑 ... } } else { sb.append(makeEmptyRow()); } return sb.toString(); // 这里需要转义吗? } ...
测试时在FTL里调用${object.renderRow("key")}能直接把返回的字符串渲染成HTML,但我没做转义处理,现在疑惑:是否需要手动转义?正确的转义方式是什么?
核心结论
是否需要转义取决于你拼接的内容是否包含用户输入或不可信数据:
- 如果
element.getNumber()或其他拼接的内容是固定值/可信后端数据,不会出现<、>、&这类HTML特殊字符,那暂时不需要转义也不会有问题。 - 如果内容来自用户输入、第三方接口或其他不可信来源,必须做转义——否则会引发XSS攻击,还可能破坏HTML结构。
正确的转义方式
不要自己手动替换特殊字符(容易遗漏),用成熟工具类或Freemarker自带能力处理:
方式1:让Freemarker接管转义(推荐)
把renderRow返回的内容改成纯文本数据结构,而非拼接好的HTML字符串,然后在FTL模板里用?html指令转义:
比如Java方法返回元素列表或空行标记,FTL里:
<#if object.hasElements("key")> <#list object.getElements("key") as element> <tr> <td>${element.number?html}</td> <!-- 其他字段同样用?html转义 --> </tr> </#list> <#else> <tr><td colspan="N"></td></tr> <!-- N是表格列数 --> </#if>
这种方式符合Freemarker最佳实践,逻辑与视图分离,代码更易维护,也不用自己处理转义细节。
方式2:在Java代码中手动转义
如果坚持要在Java里拼接HTML,用Apache Commons Text的StringEscapeUtils或Freemarker自带工具类转义:
// Apache Commons Text 示例 import org.apache.commons.text.StringEscapeUtils; sb.append(StringEscapeUtils.escapeHtml4(element.getNumber().toString()));
// Freemarker 自带工具示例 import freemarker.template.utility.StringUtil; sb.append(StringUtil.escapeHtml(element.getNumber().toString()));
转义后特殊字符会被转换成HTML实体(比如<变成<),既不会破坏HTML结构,也能防止XSS攻击。
方式3:关闭Freemarker对该变量的自动转义
如果确定内容是100%可信的HTML,可以在FTL里用?no_esc指令关闭转义:
${object.renderRow("key")?no_esc}
但这种方式风险极高,仅适合完全可控的静态内容,绝不能用于处理用户输入等不可信数据。
额外提醒
- 所有来自外部的不可信数据必须经过转义处理
- 尽量避免在Java中直接拼接HTML,保持视图逻辑在FTL模板中,代码可读性和可维护性更强
内容的提问来源于stack exchange,提问作者Arzybek

