资讯动态

JasperReports导出PDF中文消失:iText依赖冲突与字体注册全解

发布时间:2026/10/15 1:17:27 来源:尧图企业网站定制
最近维护一个基于 Spring Boot 的报表服务时我撞上了一个既经典又容易让新人懵圈的坑JasperReports 6.x 导出 PDF模板里中文全部消失本该显示汉字的地方变成了一小段空白。控制台日志里蹦出一行英文提示Unpatched iText found, cannot use glyph rendering。这句话看起来像依赖版本问题但实际排查下来它是两块独立问题的叠加一方面 classpath 里混入了没打补丁的 iText 相关类另一方面 JasperReports 根本没有注册任何中文字体。两个问题单独看都不难但混在一起很容易让你误以为“只要解决了报错中文就会显示”。这篇博文就把这条线彻底捋清楚包含我从依赖排查到字体注册、再到 Spring Boot 导出全过程的完整实操以及最后一张可以直接收藏的排障速查表。1. 先搞清楚这句英文到底在报什么为什么和汉字有关1.1 报错出现的位置和触发条件Unpatched iText found, cannot use glyph rendering 这句话的“主语”是 JasperReports 内部 PDF 导出器。JasperReports 在导出 PDF 时会去调用 iText 完成底层的 PDF 生成。这里的 iText 不是随便一个版本都能用JasperReports 依赖的是一套经过专门补丁的 iText 构建。所谓“Unpatched iText”就是导出器在运行时检测到当前 classpath 里的 iText 不符合它的预期没有包含它所需的字形渲染能力。正常情况下的英文模板、数字、纯字母不依赖这套字形渲染也能工作。但只要你开始使用带中文这类复杂字形内容的字体在文本转换阶段就会走入需要字形渲染的分支此时才发现 iText 没打补丁异常便直接抛出来了。1.2 中文消失和这个报错是两回事但常常同时出现很多项目里其实存在两个独立的问题第一JasperReports 没有可用的中文字体。它内置的默认字体大多是英文字体在无法匹配中文字形时导出器会跳过这些字符。于是 PDF 里中文区域就是空白或空格看起来像被“吃掉”了。第二一旦你为了让中文显示而去配置自定义字体就会触发字体渲染路径进而触发 Unpatched iText found。也就是说不配字体中文消失但不报错配了字体报错先一步“卡”住你。把它俩分开看你才会发现原来各自都有对应的解法。1.3 Spring Boot 项目里为什么更容易遇到不是 Spring Boot 本身有什么问题而是 Spring Boot 项目的依赖链往往很长非常容易通过某个间接依赖把未打补丁的 iText 带进 classpath。比如某个工具库里面声明了 com.lowagie:itext或者某些老项目手动加过 iText 依赖最终就出现多个 iText 共存的情况。JasperReports 在运行时检测到的可能不是自己期望的那个补丁版本于是报错。2. 先补一个基础认知JasperReports 的字体工作链路2.1 字体名、字体文件、PDF 字体是三层东西要彻底理解这个坑得先分清 JasperReports 里三个容易混淆的概念。字体名fontName在 JRXML 里写的一个逻辑名称比如 SimSun、SourceHanSansSC。它只是给模板开发人员看的一个“代号”。字体文件真正包含字形数据的 .ttf 文件也就是存储在 resources 或服务器上的实体文件。PDF 字体PDF 内部实际需要的字体资源。JasperReports 在生成 PDF 时做的事情是把“字体名”映射到“字体文件”再把它嵌入 PDF并指定字符编码。如果第一步映射没配置JasperReports 会退化到默认英文字体结果就是中文消失。2.2 中文必须满足的三个条件我排查过大量类似问题后总结出一个“中文 PDF 三件套”有一个真实存在、可被读取的中文字体文件最好是单个 TTF 文件。在 JasperReports 里注册这个字体或者至少通过 JRXML 的字体属性正确指向足够具体的字体资源。PDF 导出时指定 Unicode 编码通常使用 Identity-H并设置为嵌入字体。这三个条件任何一个不满足都会产生一类表面上相似但根因不同的故障。下面逐个给方案。3. 方案一从依赖源头解决 Unpatched iText found3.1 先用 mvn 命令看清依赖情况在排除字体问题之前先花两分钟把“Unpatched iText found”的问题解决掉。第一步永远是查看依赖树。以 Maven 为例在项目根目录执行mvn dependency:tree -Dincludescom.lowagie:itext如果输出里只有一条类似[INFO] - net.sf.jasperreports:jasperreports:jar:6.20.0:compile [INFO] | \- com.lowagie:itext:jar:2.1.7:compile说明 iText 这一块比较干净。如果出现多条尤其是有不同的版本号或者有非 jasperreports 依赖项引入的 iText那基本就能锁定问题了。用 Gradle 的项目可以两条命令。./gradlew dependencyInsight --dependency com.lowagie:itext重点看有没有两条来源一条来自 jasperreports另一条来自其他工具库。3.2 排除冲突依赖而不是盲目删 jasperreports 一部分处理方式不是把 jasperreports 自己依赖的 iText 给 exclude 掉而是去排除那个“多出来的、没有补丁的 iText”。比如你有某个第三方库 A它传递引入了 com.lowagie:itext那就在依赖库 A 的地方做局部排除dependency groupIdcom.example/groupId artifactIdlibrary-a/artifactId version1.2.0/version exclusions exclusion groupIdcom.lowagie/groupId artifactIditext/artifactId /exclusion /exclusions /dependency这样既不会影响 JasperReports 本体又能把“污染源”隔离掉。有些项目里 jasperreports 和另一个报表库共存另一个报表库带的是 iText 某个完全不兼容的版本这时候也需要以同样方式在相应依赖上做排除。3.3 验证是否消除排除完之后重新跑一次依赖树mvn dependency:tree -Dincludescom.lowagie:itext确认最终只剩 jasperreports 相关的唯一一条后再导出 PDF。如果你此时已经配置了中文字体异常就不会再出现。如果还不放心可以用一个更直接的运行时验证执行以下命令看实际加载类来源java -verbose:class YourMainClass | grep -i itext这能确认最终 JVM 从哪个 jar 加载了 iText 相关类。4. 方案二把中文字体正式注册进 JasperReports依赖层面的问题解决后真正让中文显示出来的核心操作是把中文字体做成 JasperReports 的字体扩展。这也是我认为最规范、最值得在项目里沉淀的方案。4.1 准备 TTF 字体文件你需要准备一个 .ttf 格式的中文字体。思源黑体、Noto Sans CJK 这类开源字体可以放心使用如果公司有购买授权的宋体、黑体也可以。这里要特别提醒一点不要用 .ttc 格式。JasperReports 的常规字体加载逻辑对 TTC 字体集合的支持并不好容易解析失败或只取到第一个子字体导致字体异常。如果手头只有 .ttc可以用字体工具转换成 .ttf或者直接从官方下载 TTF 版。我一般会把字体放在 src/main/resources/fonts 目录下。例如src/main/resources/fonts/SourceHanSansSC-Regular.ttf src/main/resources/fonts/SourceHanSansSC-Bold.ttf4.2 创建字体扩展配置在 src/main/resources 下新建一个文件jasperreports_extension.properties内容如下net.sf.jasperreports.extension.registry.factory.fontsnet.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory net.sf.jasperreports.extension.fonts.customfontsjasperreports_extension_fonts.xml第一行注册了一个字体扩展工厂第二行指定了字体扩展描述的 XML 文件。接着在同一目录下创建jasperreports_extension_fonts.xml内容参考?xml version1.0 encodingUTF-8? fontFamilies fontFamily nameSourceHanSansSC normal![CDATA[fonts/SourceHanSansSC-Regular.ttf]]/normal bold![CDATA[fonts/SourceHanSansSC-Bold.ttf]]/bold italic![CDATA[fonts/SourceHanSansSC-Regular.ttf]]/italic boldItalic![CDATA[fonts/SourceHanSansSC-Bold.ttf]]/boldItalic pdfEncodingIdentity-H/pdfEncoding pdfEmbeddedtrue/pdfEmbedded /fontFamily /fontFamilies这里解释几个关键配置fontFamily 的 name 属性就是你在 JRXML 里使用的逻辑字体名。normal 节点指向字体文件的 classpath 资源路径相对于 resources 根目录。italic 和 boldItalic 如果找不到真实斜体字可以暂时复用常规和粗体视觉效果上斜体不会太正但至少不会崩溃。pdfEncoding 设置成 Identity-H是让 PDF 按 Unicode 双字节编码处理字符中文必须靠它。pdfEmbedded 设为 true意思是在生成 PDF 时把字体文件嵌进去。这样部署到任何机器上 PDF 里依然是完整的中文字形不会因为目标机器没装中文字体而显示异常。4.3 在 JRXML 中正式引用自定义字体接下来在模板里使用注册的字体名。推荐用样式统一管理避免每个文本框都写一遍字体属性。先定义一个全局样式jasperReport ... style nameChineseDoc fontNameSourceHanSansSC/然后在 textElement 节点里使用这个字体。实际模板片段可以这样写textField reportElement x20 y20 width200 height30/ textElement font fontNameSourceHanSansSC pdfFontNameSourceHanSansSC pdfEncodingIdentity-H isPdfEmbeddedtrue/ /textElement textFieldExpression![CDATA[中文内容测试]]/textFieldExpression /textField这里即使你已经定义了样式我仍然建议在复杂渲染场景下把 font 属性写完整。因为某些旧版 JasperReports 或某些导出配置下样式继承可能不会把 pdf 前缀的属性完整带入。写得越明确越不会在导出时出现“字体对但 PDF 编码不对”的玄学问题。4.4 Spring Boot 导出 PDF 的完整示例假设你已经有编译好的 jrxml 或 jasper 文件导出 PDF 的 Spring Boot 代码很简单。先准备一个 Service。package com.example.report; import net.sf.jasperreports.engine.JasperCompileManager; import net.sf.jasperreports.engine.JasperExportManager; import net.sf.jasperreports.engine.JasperFillManager; import net.sf.jasperreports.engine.JasperPrint; import net.sf.jasperreports.engine.JasperReport; import net.sf.jasperreports.engine.data.JREmptyDataSource; import org.springframework.stereotype.Service; import java.io.InputStream; import java.io.OutputStream; import java.util.Map; Service public class ChineseReportService { public void exportPdf(MapString, Object params, OutputStream outputStream) throws Exception { try (InputStream stream getClass().getResourceAsStream(/reports/chinese_report.jrxml)) { JasperReport jasperReport JasperCompileManager.compileReport(stream); JasperPrint jasperPrint JasperFillManager.fillReport( jasperReport, params, new JREmptyDataSource() ); JasperExportManager.exportReportToPdfStream(jasperPrint, outputStream); } } }再写一个 Controller。package com.example.report; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; RestController public class ReportController { private final ChineseReportService reportService; public ReportController(ChineseReportService reportService) { this.reportService reportService; } GetMapping(/report/pdf) public void downloadPdf(HttpServletResponse response) throws Exception { MapString, Object params new HashMap(); params.put(title, 供应商季度对账单); params.put(company, 某科技公司); response.setContentType(MediaType.APPLICATION_PDF_VALUE); response.setHeader(HttpHeaders.CONTENT_DISPOSITION, inline; filenamereport.pdf); reportService.exportPdf(params, response.getOutputStream()); } }这里补充一个实操细节如果你的项目比较复杂日志里偶尔能看到 JRFontNotFoundException但导出的 PDF 却正常说明字体扩展没有真正被 JasperReports 的导出器加载而是走了某些回退逻辑或者你碰巧在 JRXML 里配置了系统字体路径。不要被“正常”假象骗了应该检查 jasperreports_extension.properties 是否真的出现在打包产物 classes 目录里以及 XML 中的字体路径和实际资源路径是否完全一致。5. 特殊情况下的救急办法不用扩展也能临时显示中文字体扩展是标准做法但有些临时场景下比如只排查一个问题、验证字体文件路径、或者不方便改公共模板也能直接在 JRXML 里指定系统字体路径。这种方法能快速见效但不能作为长期方案。下面给出一个简单的示例font fontNameSimSun pdfFontName/usr/share/fonts/truetype/noto/NotoSansSC-Regular.ttf pdfEncodingIdentity-H isPdfEmbeddedtrue/这个写法相当于把字体文件路径硬编码到模板中。优点是省去了字体扩展配置缺点也非常明显换一台服务器路径不存在PDF 导出直接失败。Windows、Linux、Docker 容器里的字体目录不同可移植性差。如果团队其他人复用模板他们未必知道这个字体路径是哪来的。所以它只适合你在本机快速验证“是不是字体文件本身有问题”不适合进入生产代码。还有另一种写法是使用 iText 内置的亚洲字体名称比如 STSong-Lightfont fontNameSTSong-Light pdfFontNameSTSong-Light pdfEncodingUniGB-UCS2-H/这种写法依赖环境里的东亚字体包很多精简部署环境下并不存在。再加上 UniGB-UCS2-H 这类非 Identity-H 编码对复杂字形支持有限我不是很推荐。它看着代码量少实际坑更多。6. 排障速查表与实操心得我把实际踩坑过程中遇到的高频问题整理成了一张速查表希望可以帮你省掉半天无头绪的排查。现象大概率原因处理建议PDF 中文整体消失无任何异常JasperReports 没有匹配中文字体默认用了英文字体按第 4 节注册中文字体并在 textElement 中显式使用中文变成方块、豆腐块有字体名但字体文件没嵌入或 PDF 查看器没有对应字体设置 isPdfEmbeddedtrue并把 pdfEncoding 改为 Identity-H中文显示成乱码、问号pdfEncoding 不是 Unicode 编码统一使用 Identity-H不要用 Cp1252、UniGB-UCS2-H报 Unpatched iText foundclasspath 中存在未打补丁的 iText按第 3 节做依赖排除验证依赖树仅保留一条报 JRFontNotFoundException字体扩展未生效或字体资源路径错误检查 jasperreports_extension.properties 与 XML 路径确认 names 完全一致日志提示字体找不到但 PDF 仍正常走了系统字体或回退字体掩盖了扩展配置问题删除系统字体路径写法强制走扩展验证配置真正生效导出的 PDF 文件很大嵌入了完整中文字体字体文件本身大使用字体的子集化工具精简只有用到的汉字或使用按需子集化关于“Unpatched iText found”有一点我要单独再强调一次不要在没搞清楚情况下直接升级 iText 的版本。我在项目里见过有同学把 JasperReports 依赖里的 iText 从 2.1.7 强制升级到某个高版本结果报了一堆 NoSuchMethodError。JasperReports 6.x 对 iText 的版本绑定非常敏感最佳策略是保证 classpath 里只有 JasperReports 认可的补丁版本。它不是版本越低越好也不是越高越好而是“正确的那一份”。另外一个容易被忽略的点是在 Spring Boot fat jar 里字体文件如果放在 src/main/resources 下JasperReports 通常可以正常读取因为底层使用的是 ClassLoader 资源加载。但如果你的构建脚本对资源做了过滤比如在处理 properties 或 xml 时误伤 ttf 文件就可能导致字体文件损坏。检查方法也很简单解压打出来的 jar找到 fonts 目录用文件工具看字体文件字节是否完整。我在一个老项目里就曾因为 pom 里的资源过滤配置把 ttf 给破坏了PDF 导出时中文一直不行排查了半天才发现是构建问题。最后再分享一个提升报表可维护性的技巧字体扩展文件一旦配置好项目每个模板都能复用。我习惯把中文字体扩展相关的 resources 单独放一个模块然后在所有需要导 PDF 的服务里依赖它。这样团队成员不用每个人都去理解字体的底层机制只要在模板里记住使用约定好的字体名即可。以后要换字体、更新字体文件也只需要改这一个模块不需要动业务代码。中文 PDF 导出这个坑本质上就是一个依赖管理问题加一个字体映射问题。先通过依赖树排除不该存在的 iText再用字体扩展把中文字体注册进 JasperReports你就能在 Spring Boot 项目里稳定输出带中文的 PDF。整个过程听起来步骤不少但一旦跑通你会发现它其实是完全可以标准化的基础设施。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑