Freemarker 无法转译 & 字符

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 复制代码
& 表示 &
&lt; 表示 <
&gt; 表示 >
&quot; 表示 "

当你在 XML 内容中直接使用 & 符号(而不是作为实体引用的一部分),XML 解析器会认为你要开始一个实体引用,但如果在 & 后面没有紧跟合法的实体名称,就会抛出这个异常。

这个错误经常出现在:

  1. URL 参数
  2. JavaScript 代码 嵌入在 XML 中
  3. CSS 样式 中包含 &
  4. 文本内容 中包含 & 符号(如 "AT&T" 应写成 "AT&T")

经检查,发现使用Freemarker结合模板和数据时,数据中有一个参数带有 & 符号,如下:

handlebars 复制代码
"receiptAddress": "北京北京市昌平区北七家镇东沙河村&12号:#+)()",

解析结果:

如下面解析结果所示,freemarker 并没有把 & 解析。

html 复制代码
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;联系地址:北京北京市昌平区北七家镇东沙河村&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("&", "&amp;"); // 正确转义

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&amp;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 (这就是为什么 & 必须转义为 &amp;
CSS 支持 支持 CSS 2.1(部分 CSS3),可以控制字体、颜色、布局等
基于 iText 底层依赖 iText 库(2.x 版本)生成 PDF
分页控制 支持 page-break-before/after 等分页属性
字体嵌入 可以嵌入自定义字体(解决中文显示问题)

重要注意事项

1、XHTML 必须合法

  • 所有标签必须闭合
  • 特殊字符必须转义
html 复制代码
& → &amp;, < → &lt;
  • 有且只有一个根元素

2、CSS 限制

  1. 不支持 JavaScript
  2. 部分现代 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
复杂逻辑处理 支持循环、条件判断、宏定义 生成表格、列表等复杂结构
相关推荐
自在极意功。14 小时前
简单介绍SpringMVC
java·mvc·springmvc·三层架构
superman超哥14 小时前
Rust Vec的内存布局与扩容策略:动态数组的高效实现
开发语言·后端·rust·动态数组·内存布局·rust vec·扩容策略
Evand J14 小时前
【MATLAB例程,附代码下载链接】基于累积概率的三维轨迹,概率计算与定位,由轨迹匹配和滤波带来高精度位置,带测试结果演示
开发语言·算法·matlab·csdn·轨迹匹配·候选轨迹·完整代码
Yuiiii__14 小时前
一次并不简单的 Spring 循环依赖排查
java·开发语言·数据库
tkevinjd14 小时前
JUC4(生产者-消费者)
java·多线程·juc
野槐14 小时前
java基础-面向对象
java·开发语言
sww_102614 小时前
Openfeign源码浅析
java·spring cloud
遇见~未来15 小时前
JavaScript构造函数与Class终极指南
开发语言·javascript·原型模式
foundbug99915 小时前
基于MATLAB的TDMP-LDPC译码器模型构建、仿真验证及定点实现
开发语言·matlab