
上周同事递过来一个导出PDF的工单JasperReport 6.x 项目在 JasperSoft Studio 里预览好好的一旦跑在 Spring Boot 服务里导出的 PDF 所有中文都变成空心方框控制台里不断刷出Unpatched iText found, cannot use glyph rendering这一行。第一眼看到这条提示任何人都会觉得是 iText 没打补丁的锅可我把 iText 版本翻来覆去比对、折腾了大半天最后才意识到问题从来不在 iText 身上。这篇就把完整的排查思路和可直接落地的配置写出来给正在被同样问题折磨的同学做个参照顺便理清这条提示的真实含义。1. 这行提示的真实身份不是报错更像一个环境体检报告先说结论Unpatched iText found, cannot use glyph rendering是 JasperReports 6.x 在初始化 PDF 导出环境时输出的一条提示不是异常堆栈也不代表这次导出一定失败。它之所以让你紧张是因为它的出现时机恰好和汉字全部变方框重合两者很容易被误认为因果关系。1.1 为什么从 6.11 之后这行字见得多了根据我接触过的项目来看这条提示在 6.x 早期版本里很少出现从 6.11 开始变频繁。原因是 JasperReports 更新了 PDF 渲染路径加入了一套基于字形glyph的渲染机制想用它来做更精确的文本排版和字体回退。这套机制要求底层 iText 必须是打过补丁的版本。这里说的补丁指的是 JasperReports 官方维护的 iText 分支。它和开源原生 iText 的关系可以理解成同一个库的两个构建产物功能主体一样但补丁版额外带了 JasperReports 需要的字形处理接口。项目里最常见的 Maven 坐标是com.lowagie:itext:2.1.7.js6这个带js6后缀的版本就是配套补丁版。如果你的 classpath 里加载的是没有js6后缀的原版2.1.7JasperReports 就能检测到你用的 iText 不是我要的那个定制版于是打出这行提示。比较坑的是这个提示不一定走 logback/log4j 日志框架。它经常直接打在标准输出或者应用控制台上导致你在application.yml里把日志级别调到 DEBUG 也搜不到它只有去容器控制台看原始输出才能看到。这也解释了为什么很多人排查时总觉得日志缺了一块。1.2 一个容易被忽略的依赖污染场景我在排查那个工单时第一轮就把mvn dependency:tree拉出来看了结果发现项目里同时存在两条com.lowagie:itext依赖一条来自net.sf.jasperreports:jasperreports版本是2.1.7.js6另一条来自某个操作 Word 文档的辅助库传到项目里的是原版2.1.7。两条依赖共存时实际加载哪个类版本完全取决于 classpath 顺序而这个顺序谁都没法保证。一旦原版 iText 的类先被加载JasperReports 检测到的就是Unpatched iText字形渲染直接被禁用。所以排查这个提示的第一步不是去下载补丁而是先确认自己的依赖树里有没有混入原版 iText。常见做法是给 JasperReports 的依赖加上排除项再把其他库传递进来的旧版 iText 排除掉只保留js6版本。这个动作本身花不了十分钟但能把大量后续问题消灭在源头。2. 中文变方框的完整因果链从fontName到PDF字形映射把 iText 版本统一之后我原以为问题就解决了结果导出 PDF 里的中文依然是方框。这时才意识到提示归提示真正导致汉字不显示的是另一条链路JRXML 里的fontName到 PDF 字形映射。2.1 JasperReports 渲染中文字体的完整过程理解这个问题需要先知道一段文本在导出 PDF 时经历了什么。JasperReports 拿到 JRXML 模板后会读取文本元素上的fontName属性把它解析成一个字体族对象导出 PDF 时再通过字体族找到具体的字体文件位置加载字体文件并把字符映射成字形glyph最后嵌入 PDF。整个过程可以简化成JRXML 里写了fontName宋体报表引擎在本项目已注册的字体族里查找叫宋体的字体找不到就直接回退到默认字体而默认字体通常是 DejaVu SansDejaVu Sans 不包含中文字形Unicode 到字形的映射链在这里断掉PDF 阅读器为了显示一个存在但无法映射的字符只能画一个方框。关键点在第三步JasperReports 不会主动去扫描操作系统里装了哪些字体。很多人以为服务器上装了宋体、黑体JRXML 里写fontName宋体就能生效这是个非常普遍的误解。它在开发机上偶尔能生效是因为 JVM 的 AWT 字体查找机制兜了底但在没有桌面环境的 Linux 容器里这个兜底往往是不存在的于是静默回退到 DejaVu。2.2 为什么 iTextAsian 方案在新项目里越来越不吃香网上提到中文不显示十有八九会建议你引入itext-asian依赖然后用内置字体STSong-LightUniGB-UCS2-H编码。这个方案在 JasperReports 5.x 时代确实很流行因为那时候项目普遍直接操作 iText API用内置 CID 字体最省事。但放在 JasperReports 6.x Spring Boot 的场景里我个人的经验是这个方案优先级要往后放。原因有两个一是itext-asian的 jar 需要手动安装到本地 Maven 仓库在公司内部私服管理严格的团队里会多一道审批流程二是它走的是 PDF 标准字体的 CID 引用路径字体并不真正嵌入 PDF 文件在某些 PDF 阅读器和打印场景下表现不稳定而且使用限制也比嵌入式字体多。相比之下自定义字体扩展Font Extensions才是 JasperReports 官方推荐的长期方案。它把中文字体文件直接打包进应用通过fonts.xml声明字体族JRXML 里用自定义的fontName引用字体跟随 PDF 一起嵌入。这套机制从 5.x 到 6.x 一直存在配置方式几乎没变过稳定性和可移植性都更好。2.3 一个值得先做的快速自测排查阶段我建议你先手动确认一下汉字不显示到底是哪一种表现表现说明第一排查方向空白字符占位还在字形映射失败字体族注册、字体文件是否包含中文方框或豆腐块字形存在但 PDF 渲染异常字体嵌入、pdfEncoding问号或乱码编码转换失败text 元素编码、字体文件损坏导出直接报异常字体文件加载失败路径、权限、字体文件格式我当时那个项目就属于方框这一类。看到这个表现后基本可以锁定问题在字体文件本身或字体注册配置上和 iText 补丁没有直接关系。3. Spring Boot工程里的中文字体扩展落地配置可直接照抄下面这套配置我后来在好几个 Spring Boot JasperReport 6.x 项目里重复使用过每一步都是踩过坑之后稳定下来的版本。如果你也是被这个问题卡住可以直接照着搭。3.1 准备字体文件时的一个选择OTF还是TTF中文字体我优先推荐思源黑体Source Han Sans或者文泉驿正黑。思源黑体是开源字体覆盖的汉字全字形规范导出效果接近常见的微软雅黑而且它的授权协议允许嵌入 PDF 和子集化没有版权风险。选文件格式时有个小坑思源黑体官方发布的是 OTFJasperReports 6.x 在多数情况下能识别 OTF但 OTF 里如果包含的是 CFF 轮廓部分 iText 版本在嵌入阶段会报奇怪错误。稳妥的做法是选同系列里打包好的 TTF 版本或者下载后用字体工具转换成 TTF。我自己习惯直接找SourceHanSansSC-Regular.ttf这类 TTF 文件能少很多无谓的兼容性折腾。字体文件数量上不要把全部字重都塞进去。对绝大多数报表来说准备 Regular 和 Bold 两个文件就足够应付了。斜体在中文排版里基本用不到后面会细说缺失字重会带来什么问题。3.2 工程目录、扩展注册文件与fonts.xml配置目录结构建议这么放src/main/resources/ ├── jasperreports_extension.properties └── fonts/ ├── fonts.xml ├── SourceHanSansSC-Regular.ttf └── SourceHanSansSC-Bold.ttf这个结构里有三个关键点。第一jasperreports_extension.properties必须放在 classpath 根目录也就是src/main/resources下。JasperReports 启动时会在 classpath 里自动寻找这个文件通过它来发现你注册的扩展。第二fonts.xml放在fonts/目录下。但注意fonts.xml里的字体文件路径基准也是 classpath 根目录不是src/main/resources。也就是说路径写fonts/SourceHanSansSC-Regular.ttf而不是src/main/resources/fonts/xxx.ttf。fonts.xml内容如下?xml version1.0 encodingUTF-8? !DOCTYPE fontFamilies SYSTEM http://jasperreports.sourceforge.net/dtd/fontFamily.dtd fontFamilies fontFamily nameSourceHanSans normalfonts/SourceHanSansSC-Regular.ttf/normal boldfonts/SourceHanSansSC-Bold.ttf/bold italicfonts/SourceHanSansSC-Regular.ttf/italic boldItalicfonts/SourceHanSansSC-Bold.ttf/boldItalic pdfEncodingIdentity-H/pdfEncoding pdfEmbeddedtrue/pdfEmbedded exportFonts export keynet.sf.jasperreports.htmlSourceHanSans/export export keynet.sf.jasperreports.xlsSourceHanSans/export export keynet.sf.jasperreports.csvSourceHanSans/export export keynet.sf.jasperreports.rtfSourceHanSans/export /exportFonts /fontFamily /fontFamiliesjasperreports_extension.properties内容net.sf.jasperreports.extension.registry.factory.fontsnet.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory net.sf.jasperreports.extension.fonts.font.familiesfonts/fonts.xml注意编码是 UTF-8。数据库导入、Excel 模板这类东西出乱码的案例太多配置文件这一环也别偷懒。第三pdfEncoding设置为Identity-HpdfEmbedded必须为true。Identity-H对 TTF 字体是能正常工作的一种编码方式它按 Unicode 直接映射字形适合中英文混排。如果你用的是 CID 标准字体方案那里才需要UniGB-UCS2-H但自定义字体扩展用Identity-H是最稳的。3.3 JRXML里的fontName怎么改用全局样式最省事字体扩展配置好后JRXML 里每个文本元素的fontName都要改成SourceHanSans也就是fonts.xml里声明的fontFamily name。注意这个名字必须完全一致大小写也不能错。如果模板还没生成在设计阶段就把字体统一设好最省事。JasperSoft Studio 里操作路径是选中文本元素打开属性面板Font Name 下拉框里选择 SourceHanSans。如果模板已经有很多页了一个个去改文本元素太痛苦。更好的办法是先在 Studio 里定义一个全局样式Style把它应用到所有文本元素后续想换字体只改一处即可。项目里如果是一批历史模板还可以用文本替换工具直接批量处理 JRXML 源文件把font fontName宋体/替换成font fontNameSourceHanSans/前提是团队里模板的版本控制做得比较规范。3.4 pom依赖版本统一与itext污染排查Maven 依赖里JasperReports 6.x 自身的依赖已经包含补丁版 iText所以正常情况下你不需要显式引入。但如果项目里有别的库把 iText 的旧版本传递进来了就要手动干预。给 JasperReports 的依赖加一个完整段落确保它用的 iText 是带js6后缀的dependency groupIdnet.sf.jasperreports/groupId artifactIdjasperreports/artifactId version6.20.5/version /dependency然后跑一遍mvn dependency:tree | grep itext看看有没有出现两条com.lowagie:itext。如果有用exclusions把不需要的那条排除掉。这一步做完刚才那个Unpatched iText提示通常就会消失因为补丁版 iText 重新占据了 classpath 主导位置。配置完成后重启服务重新导出。验证方法很简单拿到 PDF 文件打开文档属性 → 字体标签页如果里面列出了 SourceHanSans而且嵌入状态显示是说明字体已经成功嵌入这版配置就算跑通了。4. Linux服务器上翻车的两个隐藏杀手系统字体与临时目录缓存本地 Windows 环境把配置改完导出 PDF 一切正常我一度以为事情结束了。结果部署到 Linux 测试服务器中文又是方框而且这次连Unpatched iText提示都没了排查难度反而更高。在 Linux 环境下有两个隐藏杀手值得专门讲一讲。4.1 Linux系统本身缺中文字体但不是唯一原因第一个杀手是系统缺少中文字体。理论上自定义字体扩展机制下JasperReports 不需要依赖操作系统字体因为 TTF 文件是打包在应用里的。但问题在于项目里往往不止 JasperReports 一个组件在渲染文本。比如某些图表库、二维码组件或者打印预览的 AWT 绘制环节仍然会通过java.awt.Font去查找系统字体。一旦查不到中文字体这些环节就会默默降级或者输出乱码让人误以为是报表引擎的问题。上服务器后先跑这条命令确认fc-list :langzh如果输出为空说明系统里一个中文字体都没有。解决办法是安装系统级中文字体包或者直接把项目里的 TTF 文件复制到系统字体目录mkdir -p /usr/share/fonts/truetype/sourcehansans cp SourceHanSansSC-Regular.ttf SourceHanSansSC-Bold.ttf /usr/share/fonts/truetype/sourcehansans/ fc-cache -f -v容器镜像里也一样Dockerfile 中加上安装字体的步骤。别以为应用自己带了字体文件就算完AWT 那条链路不在你控制范围内。4.2 改了配置不生效先怀疑临时目录缓存第二个杀手是临时目录里的缓存。JasperReports 在运行过程中会在${java.io.tmpdir}下生成临时文件有些版本对字体扩展元数据做了缓存处理。表现就是你明明改了fonts.xml甚至字体文件都换了一个重启应用后导出的 PDF 还是老样子让人一度以为改错了地方。我当时的处理方式简单粗暴但也有效先把应用停掉清一下临时目录rm -rf /tmp/jasperreports然后重启应用。如果项目跑在 Docker 容器里容器重建时$TMPDIR是干净的一般不用管但如果你复用了同一个容器不重启只是改了 jar 包里的配置这个缓存就可能一直留着。为了验证是不是缓存作祟可以在重启的时候加一句临时日志输出看字体扩展注册路径到底指向哪里的文件。4.3 一个容易被误导的排查方向日志去哪了还有一点要提醒前面说过Unpatched iText found这行可能直接打到标准输出不经过日志框架。在 Linux 服务器上排查时如果只看app.log很可能什么异常都看不到但奇怪的是 PDF 里中文照样是方框。这种情况不要急着怀疑字体配置先把服务停掉用nohup或直接前台启动方式跑一次观察控制台原始输出往往能发现多出的信息。5. 进阶细节字体子集化、粗体缺失与让警告消失的执念核心问题解决之后还有几个和中文导出相关的进阶细节。这些细节不影响第一版跑通但在生产环境上线前最好都过一遍。5.1 嵌入式字体导致PDF体积暴涨子集化怎么处理中文字体文件体积大嵌入 PDF 后文件体积也随之暴涨。一个完整思源黑体 Regular 就有 8MB 以上如果模板用到大量中文文本导出 PDF 可能从几百 KB 变成十几 MB接口响应变慢存储成本也跟着涨。处理办法是字体子集化只保留模板实际会用到的那部分汉字和符号把字体文件裁剪成几百 KB 的小文件再喂给 JasperReports 字体扩展。我常用的是开源工具fontTools的pyftsubset命令pyftsubset SourceHanSansSC-Regular.otf \ --text-filechars.txt \ --output-fileSourceHanSansSC-Subset.ttf其中chars.txt是模板里出现的所有字符集合。最实用的做法不是手工录入 3500 个常用字而是把历史导出数据里出现过的人名、地名、备注字段都收集起来连同常用汉字一起放进chars.txt这样既控制体积又能覆盖实际业务场景。子集化之后的字体文件可以继续沿用fonts.xml里的配置只是把normal路径指向裁剪后的文件即可。5.2 粗体与斜体缺失一个低报错率的隐蔽坑如果fonts.xml里只注册了normal没有注册boldJasperReports 在遇到设置了boldtrue的文本元素时行为很微妙它不一定报错而是在日志里留一句字体族找不到对应变体的提示然后用普通字重渲染。最终导出的 PDF 里本该加粗的文字全部是常规字重如果没人仔细核对这个坑能潜伏很久。所以配置字体族时尽量把bold也配好至少让boldItalic指向和bold同样的字体文件。斜体如果是中文一般不会真正倾斜直接映射回normal也没问题但至少别让这个变体成为空引用。5.3 到底要不要为了消除提示而换iText版本有人执着于让Unpatched iText found从日志里彻底消失甚至考虑把 iText 换成商业授权版本。我的观点是不要为了一条提示做出超出实际需要的改动。这条提示真正影响的是基于字形的高级渲染这个能力而不是基础的中文字体嵌入。如果你当前的场景是常规中文报表字体已经正确注册嵌入PDF 在主流阅读器里都能正常显示那么这条提示更像一个环境兼容性信息对业务没有任何影响。反过来如果你只是把 iText 换成商业版却没有把字体扩展配置对中文照样不显示钱花了问题还在。真正需要关注这条提示的场景是遇到复杂的文本排版需求比如特殊组合字符、复杂脚本文本渲染或者用户反馈某些字符在某个 PDF 阅读器里渲染不对。这种时候才值得去检查 iText 补丁版本是否确实缺失。5.4 模板里styled标记导致的加粗失效最后一个冷门问题JRXML 里如果文本元素设置了markupstyled并且内容里用了b标签那么渲染时它会强制查找字体族的 bold 变体。如果fonts.xml里bold缺失或指向错误报表引擎会按照找不到变体的逻辑处理结果就是在线预览和导出 PDF 里加粗效果不一致。排查时如果遇到 预览里是加粗的导出的 PDF 却没有加粗 这种诡异情况先看markup属性再看字体族的变体配置别去动文本内容本身。经历过这次排查之后我反而觉得Unpatched iText found是一个信号而不是故障本身。它提醒你环境里的 iText 版本可能不是你预期的版本然后再顺着这条线去查字体加载链路。现在我的排查顺序已经固定下来了先看依赖树里有没有重复的 iText再看 PDF 输出的字体列表确定字体是否嵌入最后才去翻控制台的原始输出。顺序对了大部分中文不显示的问题都能在半小时内定位而不是像我第一次那样在无关的问题上耗掉一整天。