Freemarker 无法转译 & 字符
前言
项目中使用到 Freemarker 模板引擎,将数据和 html 模板结合,动态生成 xml。然后使用 org.xhtmlrenderer.pdf.ITextRenderer 将xml 生成 PDF。但是现在出现了报错:
handlebars
org.xhtmlrenderer.util.XRRuntimeException: Can't load the XML resource (using TRaX transformer). org.xml.sax.SAXParseException; lineNumber: 54; columnNumber: 114; The entity name must immediately follow the '&' in the entity reference.
at org.xhtmlrenderer.resource.XMLResource$XMLResourceBuilder.createXMLResource(XMLResource.java:191)
at org.xhtmlrenderer.resource.XMLResource.load(XMLResource.java:75)
at org.xhtmlrenderer.pdf.ITextRenderer.setDocumentFromString(ITextRenderer.java:158)
at org.xhtmlrenderer.pdf.ITextRenderer.setDocumentFromString(ITextRenderer.java:153)
异常原因
这个错误是因为 XML 中的 & 符号没有正确转义导致的。
在 XML 中,& 是一个特殊字符,用于表示实体引用(Entity Reference),例如:
xml
& 表示 &
< 表示 <
> 表示 >
" 表示 "
当你在 XML 内容中直接使用 & 符号(而不是作为实体引用的一部分),XML 解析器会认为你要开始一个实体引用,但如果在 & 后面没有紧跟合法的实体名称,就会抛出这个异常。
这个错误经常出现在:
- URL 参数
- JavaScript 代码 嵌入在 XML 中
- CSS 样式 中包含 &
- 文本内容 中包含 & 符号(如 "AT&T" 应写成 "AT&T")
经检查,发现使用Freemarker结合模板和数据时,数据中有一个参数带有 & 符号,如下:
handlebars
"receiptAddress": "北京北京市昌平区北七家镇东沙河村&12号:#+)()",
解析结果:
如下面解析结果所示,freemarker 并没有把 & 解析。
html
<p> 联系地址:北京北京市昌平区北七家镇东沙河村&12号:#+)()</p>
由于这个是在 Java 项目中生成 PDF 时出现的错误,通常是因为 动态生成的 XHTML/XML 内容中包含未转义的 & 符号,而 PDF 生成库(如 Flying Saucer/iText)在解析时将其视为无效的 XML 实体引用。
所以,根源在于 Freemarker 生成模板数据时未转译 &。
示例:
java
// 常见错误场景:动态拼接 HTML 内容
String html = "<html><body>"
+ "<p>访问链接: http://example.com?id=123&name=abc</p>" // ❌ 这里的 & 未转义
+ "</body></html>";
// 生成 PDF 时
ITextRenderer renderer = new ITextRenderer();
renderer.setDocumentFromString(html); // 抛出 XRRuntimeException
解决方法
解决转译问题方法有很多种。
1. 手动转义(快速修复)
直接在模板和数据结合之前,将 & 符号转译:
java
String receiptAddress = "北京北京市昌平区北七家镇东沙河村&12号:#+)()";
String safeAddress = receiptAddress.replace("&", "&"); // 正确转义
2. 使用 Apache Commons Lang 转义工具(推荐)
更全面地转义所有 XML 特殊字符:
原始的 Freemarker 配置代码如下:
java
import org.apache.commons.lang3.StringEscapeUtils;
String content = "http://example.com?id=123&name=abc";
String safeContent = StringEscapeUtils.escapeXml10(content); // 转义 & < > " '
// 输出: http://example.com?id=123&name=abc
3. 在模板引擎中处理
使用 Freemarker 时出现这个错误,通常是因为模板中关闭了自动转义或者手动使用了不转义指令。
java
@Configuration
public class FreemarkerConfig {
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/resource/createpdf/");
Properties settings = new Properties();
settings.setProperty("default_encoding", "UTF-8");
settings.setProperty("template_update_delay", "3600");
configurer.setFreemarkerSettings(settings);
return configurer;
}
}
上述配置并没有开启自动转译,修改如下:
java
import freemarker.core.HTMLOutputFormat;
import freemarker.template.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import java.util.Properties;
@Configuration
public class FreemarkerConfig {
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/resource/reporttemplate/");
Properties settings = new Properties();
settings.setProperty("default_encoding", "UTF-8");
settings.setProperty("template_update_delay", "0");
// 增加这行:开启 HTML 自动转义
settings.setProperty("output_format", "HTML");
configurer.setFreemarkerSettings(settings);
return configurer;
}
}
但是在启动项目时出现了报错:
handlebars
2026-01-09 16:12:22.313 53516 [main] ERROR
o.s.boot.SpringApplication - Application startup failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'freeMarkerConfigurer' defined in class path resource [com/project/user/config/FreemarkerConfig.class]: Invocation of init method failed; nested exception is freemarker.core.Configurable$SettingValueAssignmentException: Failed to set FreeMarker configuration setting "output_format" to value "HTML"; see cause exception.
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1628)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
Caused by: freemarker.core.Configurable$SettingValueAssignmentException: Failed to set FreeMarker configuration setting "output_format" to value "HTML"; see cause exception.
at freemarker.core.Configurable.settingValueAssignmentException(Configurable.java:2664)
at freemarker.template.Configuration.setSetting(Configuration.java:3117)
at freemarker.core.Configurable.setSettings(Configurable.java:2714)
at org.springframework.ui.freemarker.FreeMarkerConfigurationFactory.createConfiguration(FreeMarkerConfigurationFactory.java:269)
at org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer.afterPropertiesSet(FreeMarkerConfigurer.java:116)
Caused by: java.lang.IllegalArgumentException: null
at freemarker.core._ObjectBuilderSettingEvaluator$BuilderCallExpression.getStaticFieldValue(_ObjectBuilderSettingEvaluator.java:960)
at freemarker.core._ObjectBuilderSettingEvaluator$BuilderCallExpression.eval(_ObjectBuilderSettingEvaluator.java:886)
at freemarker.core._ObjectBuilderSettingEvaluator.ensureEvaled(_ObjectBuilderSettingEvaluator.java:161)
at freemarker.core._ObjectBuilderSettingEvaluator.eval(_ObjectBuilderSettingEvaluator.java:129)
at freemarker.core._ObjectBuilderSettingEvaluator.eval(_ObjectBuilderSettingEvaluator.java:106)
查询是因为 freemarker 版本太低,不支持这种配置。
org.xhtmlrenderer.pdf.ITextRenderer 是什么
org.xhtmlrenderer.pdf.ITextRenderer 是 Flying Saucer 库的核心类,专门用于将 XHTML + CSS 内容渲染成 PDF 文件。 是一个"HTML 转 PDF"的桥梁,它的 XML 严格性要求导致你必须正确处理 & 等特殊字符。
核心作用
简单来说:你给它一段 HTML 字符串,它帮你生成 PDF 文件。
java
// 典型使用流程
ITextRenderer renderer = new ITextRenderer();
renderer.setDocumentFromString("<html><body><h1>Hello</h1></body></html>");
renderer.layout();
renderer.createPDF(outputStream); // 输出 PDF
关键特性
| 特性 | 说明 |
|---|---|
| XML 严格性 | 要求 XHTML 必须是格式良好的 XML (这就是为什么 & 必须转义为 &) |
| CSS 支持 | 支持 CSS 2.1(部分 CSS3),可以控制字体、颜色、布局等 |
| 基于 iText | 底层依赖 iText 库(2.x 版本)生成 PDF |
| 分页控制 | 支持 page-break-before/after 等分页属性 |
| 字体嵌入 | 可以嵌入自定义字体(解决中文显示问题) |
重要注意事项
1、XHTML 必须合法
- 所有标签必须闭合
- 特殊字符必须转义
html
& → &, < → <
- 有且只有一个根元素
2、CSS 限制
- 不支持 JavaScript
- 部分现代 CSS 特性不支持(如 flexbox、grid)
3、字体问题
java
// 解决中文不显示问题
renderer.getFontResolver().addFont(
"classpath:/fonts/SimSun.ttf",
BaseFont.IDENTITY_H,
BaseFont.NOT_EMBEDDED
);
底层原理

与 Freemarker 的关系
org.xhtmlrenderer.pdf.ITextRenderer 是 Flying Saucer 项目的一部分,与 Freemarker 完全独立,是两个不同的库。它们的关系(协作而非归属)。

Freemarker 的作用是什么
Freemarker 是一个模板引擎,核心作用是将 "数据" 和 "模板" 结合,动态生成文本内容(HTML、XML、邮件等)。
一句话理解就像做填空题:模板是带空格的试卷,数据是答案,Freemarker 帮你自动填好生成完整试卷。
核心工作原理

示例:
java
// 1. 数据
Map<String, Object> data = new HashMap<>();
data.put("name", "张三");
data.put("age", 25);
// 2. 模板 (template.ftl)
// <p>姓名:${name},年龄:${age}</p>
// 3. Freemarker 处理 → 生成结果
// <p>姓名:张三,年龄:25</p>
主要作用
| 作用 | 说明 | 示例 |
|---|---|---|
| 动态内容生成 | 根据数据变化生成不同文本 | 生成个性化邮件、报表 |
| 逻辑与表现分离 | 程序员准备数据,设计师写模板 | 前端只改模板,不动 Java 代码 |
| 跨平台输出 | 同一数据可生成 HTML、XML、JSON 等 | 一套数据生成网页和 PDF |
| 复杂逻辑处理 | 支持循环、条件判断、宏定义 | 生成表格、列表等复杂结构 |