ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JasperReports PDF中文乱码解决:字体映射与编码排查全指南

JasperReports PDF中文乱码解决:字体映射与编码排查全指南

做报表的人迟早会在某个工单里看到这句话:“PDF 里的中文变成方块了。”我上次遇到这个问题是在一个财务导出功能里,本地预览一切正常,打包部署到 Linux 服务器后,所有中文字全部变成长方形的框。排查了大半天,最后发现既不是数据乱码,也不是模板写错,纯粹是 JasperReports 的 PDF 字体映射没处理好。这类问题几乎每个做 Jasper 报表开发的人都会碰到,所以我今天把根因、配置、代码、排查思路一次性整理出来,希望能帮看到这篇的人少走一段弯路。

1. 先看现象:中文报表乱码到底乱在哪

1.1 方框、问号、空白,分别是什么原因

不同乱码形态对应的病因完全不同,千万不要一上来就改代码。我习惯先看渲染出来的 PDF 长什么样:

  • 中文变成一个个空心方框,也就是俗称的“豆腐块”,通常是字体缺失或字体没有嵌入 PDF 引擎。PDF 里根本没有可用中文字形,查看器只能画一个方框占位。
  • 中文变成问号,很多时候是编码转换问题,比如 JRXML 文件本身是 GBK,但编译时当成 UTF-8 读取;或者数据库连接拿到的字符串在驱动层已经变成“?”。
  • 中文直接不显示,连方框都看不到,可能是字体设置了isPdfEmbedded="false",同时查看器又加载不到对应字体。
  • 中文全乱成一串无意义字符,常见于字体编码写错,比如用了Cp1252这类拉丁编码去渲染中文字符。

记住这个对应关系之后,排查就能少翻很多源代码。尤其是“方框”和“问号”,很多人混为一谈,结果折腾半天字体,问题根本没在字体上。

1.2 为什么 PDF 导出最容易踩坑,HTML 却没事

接触过 JasperReports 的人应该都有这种感觉:同一个 JRXML 模板,用 HTML Preview 看是正常的,一旦导出 PDF 就乱码。原因在于 HTML 渲染依赖的是浏览器和操作系统字体,而 PDF 引擎走到了 iText 这一套字体机制里。

JasperReports 生成 PDF 时,默认字体是 Helvetica,这款字体是 PDF 标准内置的拉丁字体,根本不包含中文字形。如果你在模板里没有显式给文本元素指定中文字体,iText 只能拿 Helvetica 去渲染中文字符,结果是必然乱码。而 HTML 输出时,浏览器遇到中文字符会自己找操作系统的字体来完成渲染,所以看起来正常。这就解释了为什么“HTML 正常、PDF 乱码”这么常见。

1.3 开发环境正常、生产环境乱码的隐藏差异

另一个高频场景是本地预览没问题,部署到服务器后全乱。这里面的差异点很多:

  • 开发机器上装了宋体、黑体等中文字体,服务器的 Linux 镜像里可能一个中文字体都没有。
  • IDE 构建时默认使用 UTF-8 编译,而服务器上的构建脚本或 Docker 镜像里的 JVM 启动参数可能用了系统默认编码。
  • 字体扩展的jar包没有被打进最终部署包里,fonts.xml和字体文件在 classpath 里缺失。
  • JRXML 开发时用的字体名是 “宋体”,但生产环境没有该字体,也没有字体扩展兜底。

这类问题最坑的是“看起来一模一样,只有环境不同”,所以排查时不要只看代码,也要看部署产物的内容。

2. 核心配置:把中文字体注册给 JasperReports PDF 引擎

2.1 最快方案:在 JRXML 里直接指定中文字体

如果你只是想快速让某几个文本字段正常显示,直接在 JRXML 里声明字体即可。比如一个文本字段可以这样写:

<textElement> <font fontName="SimSun" pdfFontName="STSong-Light" pdfEncoding="UniGB-UCS2-H" isPdfEmbedded="false"/> </textElement>

这里的pdfFontName是写给 PDF 引擎的,fontName是用于 IDE 设计预览的逻辑名称。上面这种方式走的是 iTextAsian 内置字体,PDF 里不需要嵌入字体文件,但依赖itextasian.jar,并且某些 PDF 查看器可能找不到对应字体,兼容性一般。我一般是把它当应急方案用,长期项目不推荐。

更稳妥的方式是指定一个已经注册过的系统字体路径或字体名称。比如:

<font fontName="NotoSansSC" pdfFontName="NotoSansSC" pdfEncoding="Identity-H" isPdfEmbedded="true"/>

这个写法有一个硬前提:NotoSansSC必须在 JasperReports 的字体注册表里存在。如果不存在,运行时会回退到默认字体,还是乱码。

2.2 一劳永逸:用字体扩展全局注册

在大型项目里,一个报表可能有几十个字段,总不能每个都去写pdfFontName。这时候就要用 JasperReports 的字体扩展机制,把自定义字体注册成一个“字体族”,之后 JRXML 里只需要写fontName="你定义的字体族名",PDF、HTML、Excel 都会自动使用对应设置。

做法分三步。

第一步,把字体文件放进src/main/resources/fonts/目录,比如放一个NotoSansSC-Regular.otf和NotoSansSC-Bold.otf。

第二步,在资源目录下新建一个fonts.xml,内容大致如下:

<?xml version="1.0" encoding="UTF-8"?> <fontFamilies> <fontFamily name="NotoSansSC"> <normal>fonts/NotoSansSC-Regular.otf</normal> <bold>fonts/NotoSansSC-Bold.otf</bold> <italic>fonts/NotoSansSC-Regular.otf</italic> <boldItalic>fonts/NotoSansSC-Bold.otf</boldItalic> <pdfEncoding>Identity-H</pdfEncoding> <pdfEmbedded>true</pdfEmbedded> <exportFonts> <export key="net.sf.jasperreports.html">NotoSansSC</export> <export key="net.sf.jasperreports.xls">NotoSansSC</export> </exportFonts> </fontFamily> </fontFamilies>

这里把pdfEncoding设为Identity-H,对中文字符支持更完整,比UniGB-UCS2-H更适合现代字体。pdfEmbedded设为true表示把字体嵌进 PDF,这样换机器查看也不会消失,代价是 PDF 文件体积会变大。

第三步,在资源目录下新建jasperreports_extension.properties,内容如下:

net.sf.jasperreports.extension.registry.factory.fonts=net.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory net.sf.jasperreports.extension.simple.font.families.myfonts=fonts/fonts.xml

属性里的myfonts这个名字可以随便改,关键是前面的前缀不能错。这个文件会被 JasperReports 自动读取,然后把你定义的字体族加载进来。

当这三个文件都在 classpath 里后,JRXML 里的文字元素可以这样写:

<font fontName="NotoSansSC"/>

后续导出 PDF 时,JasperReports 会自动根据fontName找到字体族,再使用字体族里的pdfEncoding和pdfEmbedded设置生成 PDF。这才是最推荐的做法。

2.3 代码动态报表如何注册字体

有些项目不走 JRXML 模板,而是用JasperDesign在 Java 代码里动态拼报表。这种情况也可以用代码注册字体。

可以通过 iText 的FontFactory先注册字体文件,再设置样式字体名:

FontFactory.register("C:\\fonts\\NotoSansSC-Regular.otf", "NotoSansSC"); Style style = new Style(); style.setName("MyStyle"); Font font = new Font("NotoSansSC", 12F, Font.BOLD); style.setFont(font);

然后把<style>挂到JasperDesign上,报表字段引用style="MyStyle"即可。要注意的是动态报表里字体设置的优先级很容易混乱,如果你同时设置了element.setStyleNameReference和文本元素的属性,后者可能覆盖前面。实操时建议只从一条链路设置字体,不要既在 Style 里设、又在 TextElement 里设。

如果你用的是JRPdfExporter导出现成报表,还可以在导出时给整个报表指定默认字体:

JRPdfExporter exporter = new JRPdfExporter(); exporter.setParameter(JRPdfExporterParameter.FONT_NAME, "NotoSansSC"); exporter.setParameter(JRPdfExporterParameter.IS_PDF_EMBEDDED, Boolean.TRUE);

但这个方法只对没有再单独指定字体的元素生效。已经写了字体名的字段,还是会用它们自己的设置。

2.4 依赖问题:itextasian 到底要不要加

很多老项目会告诉你必须在 pom 里加 iTextAsian:

<dependency> <groupId>com.lowagie</groupId> <artifactId>itextasian</artifactId> <version>1.5.2</version> </dependency>

这个依赖解决的是上文中提到的STSong-Light这类内置字体名称的解析问题。如果你用的是自定义字体扩展,并且把字体文件放进了 classpath,实际上并不需要 iTextAsian,因为 iText 可以自己读出 TTF/OTF 里的字形。不过,如果项目始终用STSong-Light+UniGB-UCS2-H的方式,那就必须保证这个 jar 存在。

需要留个心眼:不同 JasperReports 版本内部依赖的 iText 不一样,有的用 2.x,有的用 5.x,对应的 itextasian 坐标也会变。如果依赖冲突导致异常,建议先看看当前运行时jarpdfexporter使用的 iText 版本,再决定用哪个坐标。不要盲从网上抄的依赖,否则可能搞出一个“版本能用,但字体映射不认识”的新坑。

3. 字体文件准备:TTF 与 TTC 的坑,以及开源字体推荐

3.1 为什么 Windows 自带字体有时是个坑

很多初学者图省事,直接把 Windows 的C:\Windows\Fonts\simsun.ttc放进项目里用。这个做法不一定跑得通,因为.ttc是字体集合文件,里面可能包含多个字体,而 iText 对 TTC 的支持没有对 TTF/OTF 那么直接。实际使用中,你用FontFactory.register("simsun.ttc", "SimSun")可能会注册失败,或者注册后字体解析出来的字形数量不对,渲染出来的中文笔画缺失。

更麻烦的是,不同 Windows 版本自带的 simsun 字体名字可能是 “宋体” 或 “SimSun”,在 JRXML 里写哪个都不一定能命中你想要的 TTF 文件。如果项目要跨平台部署,Windows 系统字体并不适合作为基础资源。

3.2 推荐使用开源中文字体

我现在的项目里统一使用思源黑体或者 Noto Sans CJK,基本都是开源字体,可以随应用一起分发,不用担心字体版权问题。具体到实现,思源黑体有 OTF 和 TTC 两种格式,下载时优先选 OTF 或单字体 TTF。Noto Sans SC 是 Google 的发行版本,通常能找到 TTF 或 OTF,直接塞进fonts/目录即可。

如果觉得思源黑体文件太大,也可以考虑思源宋体、文泉驿微米黑等开源中文字体。选择标准只有三个:有 TTF/OTF 格式、允许再分发、字形覆盖范围足够。

3.3 字体家族里的 normal、bold、italic 分别指什么

在 fonts.xml 里,<normal>、<bold>、<italic>、<boldItalic>分别对应不同字重。JasperReports 在渲染时,会根据文本元素的粗体和斜体设置去找对应的字体文件。如果你的字体只提供 regular 和 bold,那 italic 最好也指向 regular 或 bold,不要让某个状态空缺。如果空缺,JasperReports 可能会尝试合成斜体,或直接回退到默认字体,从而引起局部乱码。

很多人在字体扩展里只写了<normal>,结果报表中某个字段设置了粗体后,字体回退到默认,中文又变“豆腐块”。这个问题排查起来特别隐蔽,因为大部分字段看着是正常中文,只有特定字重的字段出问题。

3.4 字体嵌入导致文件变大,别被问懵

开启pdfEmbedded后,PDF 文件体积可能从几十 KB 暴涨到几 MB。有的业务人员会来问“怎么导出的 PDF 突然这么大”,不用紧张,这是中文字形被完整嵌入导致的正常现象。如果 PDF 只是临时预览、不需要长期保存,可以关掉嵌入,但中文渲染就依赖查看器本地字体,不同机器上可能出现差异。我的建议是在内部系统里保持嵌入,对外发出的正式单据也尽量嵌入,避免客户打开文件后中文空白。

4. 编码链路排查:从 JRXML 到数据库到 JVM 的全局字符集

4.1 JRXML 文件本身到底存了什么

字体配置正确,并不代表乱码一定消失。还有一条更隐蔽的链路是编码问题。

JRXML 本身是一个 XML 文件,第一行通常会写:

<?xml version="1.0" encoding="UTF-8"?>

但如果你的 IDE 或文本编辑器实际把文件存成了 GBK,哪怕第一行写着 UTF-8,编译器以 UTF-8 读取时也会出现乱码。这个乱码会在编译报表时被固化进.jasper文件里,最终渲染出来的 PDF 中文全乱。

我遇到过一种情况:文件用 IDEA 打开看着完全正常,因为 IDEA 自动用 GBK 解码了;但 Maven 编译时按 UTF-8 读取,编译后的模板中文已经变成问号。要避免这个问题,需要统一所有参与编译的机器和工具链编码。在pom.xml里可以显式设置项目编码:

<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>

同时确认 JRXML 文件保存格式确实为 UTF-8。这个可以从 IDE 右下角或者file命令看出来。

4.2 数据源里的中文为什么会变成问号

如果模板本身正常,但数据库字段里的中文导出后变成???,那问题多半在 JDBC 连接参数。比如 MySQL 连接串需要加上:

?useUnicode=true&characterEncoding=UTF-8

PostgreSQL 一般也建议在连接参数里明确?characterEncoding=UTF-8。这些参数虽然不影响报表引擎,但影响数据在传输过程中的编码转换。如果连接层已经把中文变成问号,后面再改字体也无济于事。

另一个容易忽略的是 CSV 导出。CSV 文件如果没有指定编码,用 Excel 打开经常乱码,因为 Excel 默认按 ANSI 解析。JasperReports 导出 CSV 时需要设置编码参数:

JRCsvExporter exporter = new JRCsvExporter(); exporter.setParameter(JRCsvExporterParameter.CHARACTER_ENCODING, "UTF-8");

更彻底的办法是导出为带 UTF-8 BOM 的文件,这样 Excel 也能正确识别。

4.3 JVM 默认字符集的影响

JVM 在启动时会读取file.encoding作为默认字符集,如果服务器是 Linux,系统 locale 可能是POSIX或C,默认编码可能不是 UTF-8。JasperReports 内部很多地方会隐式调用字符串转字节的逻辑,如果默认编码不对,模板或参数里的中文字符就容易丢。

生产环境启动应用时,最好在 JVM 启动参数里明确加上:

-Dfile.encoding=UTF-8

不要依赖系统环境。尤其项目跑在 Docker 容器里时,镜像的/etc/locale可能根本没配,JVM 很可能拿到的默认编码是 ANSI_X3.4-1968,也就是 ASCII 的近亲,中文不乱才怪。

4.4 导出代码里还有一层编码参数

使用JRPdfExporter之类导出时,也可以通过参数强制编码。典型的写法:

exportParameters.put(JRExporterParameter.CHARACTER_ENCODING, "UTF-8");

在 JasperReports 较新的 API 中,JRPdfExporter使用SimplePdfExporterConfiguration或SimplePdfReportConfiguration来设置配置项。比如:

SimplePdfExporterConfiguration config = new SimplePdfExporterConfiguration(); config.setForceLineBreakPolicy(false); exporter.setConfiguration(config);

如果你用的不是最新 API,直接查你当前 JasperReports 版本对应的 Exporter 参数类即可。重点只有一个:CHARACTER_ENCODING必须和 JRXML 声明的编码一致,否则模板里的中文字符可能在导出阶段二次转码出错。

5. 常见乱码场景速查表与排查顺序

5.1 问题速查表

下面这个表是我平时排查乱码问题的第一份参考,建议大家收藏。

现象最可能原因处理方式
PDF 中文显示为方框PDF 字体没注册或没嵌入配置字体扩展,使用中文字体族,开启嵌入
PDF 中文显示为问号数据流编码不对,不是字体问题检查 JRXML 保存编码、JDBC 连接参数
开发环境正常,生产环境乱码字体资源未打包或服务器缺字体把字体文件和 fonts.xml 打进 classpath
只有粗体或斜体字段乱码字体族没有配置对应字重在 fonts.xml 中补全 bold/italic 映射
HTML 预览正常,PDF 乱码缺少 PDF 字体定义添加 pdfFontName 或字体扩展
Excel 导出中文乱码缺少 exportFonts 映射或 POI 字体问题在 fonts.xml 中添加 xls 导出字体映射
报表模板编译后中文变问号源文件编码和编译编码不一致统一为 UTF-8,设置 maven 项目编码
CSV 用 Excel 打开乱码CSV 编码不是带 BOM 的 UTF-8设置CHARACTER_ENCODING,生成 BOM

5.2 按优先级排查的顺序

如果你是第一次遇到这种问题,别急着改代码,按下面这个顺序来,大概率能定位到根因:

  • 第一步,先确定乱码形态。方框、问号、空白,分别对应不同方向。
  • 第二步,查看 JRXML 里的字体设置。如果字段没有指定任何中文字体,先加字体扩展。
  • 第三步,确认字体扩展是否真的生效。可以把fonts.xml里的字体名改成项目里独有的名字,如果报表里用了这个名字,IDE 或者导出端能识别,说明加载成功。
  • 第四步,检查部署包的 classpath 里有没有字体文件和jasperreports_extension.properties。不要只看本地 IDE 能跑,直接解压 jar 看内容。
  • 第五步,检查 JVM 启动参数和数据源连接串的编码设置。
  • 第六步,如果还不行,写一个最简单的空白报表,只放一个“中文测试”文本,逐步加条件复现。

我自己用这个顺序排查,百分之八十的问题都在第二和第三步解决,剩下的大多数是 JVM 编码问题。

6. 我的实操经验:一次完整配置和几个容易忽略的点

6.1 项目结构里到底要放哪些文件

下面是一个最小可运行的资源结构,大家可以直接照着建:

src/main/resources/ ├── fonts/ │ ├── NotoSansSC-Regular.otf │ └── NotoSansSC-Bold.otf ├── fonts.xml ├── jasperreports_extension.properties └── reports/ └── finance_report.jrxml

jasperreports_extension.properties里只有两行,指向fonts.xml。这个文件会被 JasperReports 的扩展机制自动发现,不需要在代码里手动加载。一定要记住fonts.xml里的字体路径是相对于 classpath 根目录的,所以写fonts/NotoSansSC-Regular.otf而不是C:/fonts/...,不然换一台机器就废了。

6.2 容易忽略的五个细节

第一,凡是把字体文件放进src/main/resources的项目,构建时最好检查打包插件是否会把.otf/.ttf文件带进最终包里。某些 Maven 配置会强行过滤非 class 文件,导致字体丢了。

第二,JasperSoft Studio 里预览正常不代表运行环境正常。Studio 自带一堆字体,但你的项目里可能没有。所以设计报表时尽量只使用项目内已注册的字体族名,不要依赖 Studio 内置字体。

第三,fonts.xml里如果写了多个字体族,确保每个字体族都有唯一的<normal>路径。路径写错 JasperReports 不一定会报错,可能只是字体解析失败并回退,这种“静默失败”很坑。

第四,如果使用了自定义字体名,比如fontName="NotoSansSC",在 JRXML 编辑器中这个字体名可能显示红色提示,因为 IDE 不认识。不用慌,运行时能识别就行。

第五,导出 PDF 时不要滥用JRPdfExporterParameter里的过时参数。新版本中很多参数已经被移除或改名,老代码拷过来会直接抛异常。最好先看看项目的 JasperReports 版本,再选择对应的 API。

6.3 我的最终建议

如果你正在开发一个新的 Jasper 报表项目,从一开始就把字体扩展做进去,别等到用户反馈乱码再补。字体扩展机制虽然第一次配置起来麻烦一点,但一次配置,所有报表、所有导出格式都能吃上,比在几十个 JRXML 里手动写pdfFontName靠谱太多了。

我个人现在接到乱码工单,已经习惯了先解压部署包,看fonts.xml是否真的在 jar 里,看jasperreports_extension.properties是否完整。这两样东西只要在,“豆腐块”问题基本就消灭掉一半。剩下的字符集问题,再顺着 JRXML 编码、JDBC 串、JVM 参数这三个点一个个过,早晚能把根因揪出来。希望这篇经验也能帮你的报表少一个“方框工单”。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进