Java 加载自定义字体失败?从系统 fontconfig 到 Maven 损坏的全链路排查指南
📌 背景
SpringBoot3.5.3
jdk17
Font.createFont() 调用,报错:
java.io.IOException: Problem reading font data
java.lang.NullPointerException: Cannot load from short array because "sun.awt.FontConfiguration.head" is null
更诡异的是:同样的代码、同样的 JAR 包,在测试环境正常,生产环境却失败!
本文从 系统层、构建层、代码层 三方面,完整复盘一次真实生产事故的排查过程,最终定位到 fontconfig 缺失 + Maven 资源损坏 的双重问题,并提供可落地的解决方案。
🧩 问题现象
使用 Font.createFont(Font.TRUETYPE_FONT, inputStream) 加载 msyhl.ttc(微软雅黑轻型)
测试环境正常,正式环境报错:Problem reading font data
JAR 包相同,JDK 版本一致(OpenJDK 17)
字体文件存在于 resources/fonts/,且能通过 getResourceAsStream 读取(is != null)
🔍 排查思路:从底层到上层
我们采用 “自底向上” 的排查方法,优先检查系统依赖,再看构建过程,最后分析代码逻辑。
第一步:检查系统级字体支持 —— fontconfig 是否安装?
这是最容易被忽略,但最关键的一环!
❓ 为什么 fontconfig 如此重要?
Java(尤其是 OpenJDK)在 Linux 环境下依赖系统级的字体管理库:
fontconfig:负责字体发现、匹配、缓存
freetype:负责字体解析和渲染
如果新服务器是 最小化安装(如 CentOS minimal、Docker 镜像),很可能 默认未安装 fontconfig,导致 JVM 无法正常解析 .ttc 等复杂字体。
✅ 验证命令:
# 检查是否安装
rpm -q fontconfig || echo "❌ fontconfig 未安装"
rpm -q freetype || echo "❌ freetype 未安装"
# 查看字体缓存
fc-list | grep -i "yahei\|sim"
🛠 解决方案:
# 安装字体支持库
sudo yum install -y fontconfig freetype
# 更新字体缓存(关键!)
sudo fc-cache -fv
✅ 安装后无需重启 JVM,Java 程序即可正常加载字体。
第二步:检查 Maven 构建过程 —— 字体文件是否被“损坏”?
即使系统 fontconfig 正常,Maven 构建过程也可能破坏字体文件。
❓ 为什么 Maven 会“损坏”字体?
Maven 默认以 文本模式 处理资源文件,可能对 .ttc、.ttf 等二进制文件进行:
换行符转换(\n → \r\n)
字符编码转换
${} 占位符替换
这些操作会 破坏字体的二进制结构,导致 createFont() 失败。
✅ 验证方法:
提取 JAR 中的字体文件,检查大小:
jar -xf your-app.jar BOOT-INF/classes/fonts/msyhl.ttc
ls -l BOOT-INF/classes/fonts/msyhl.ttc
✅ 正常大小:约 9.3MB(9844184 字节)
❌ 异常大小:几十 KB 或几百 KB → 被 Maven 损坏
🛠 解决方案:在 pom.xml 中使用 保护字体文件不被 Maven 损坏
<build><resources><!-- 资源1: 所有资源默认参与过滤,但排除 fonts/ 和特定配置文件 --><resource><directory>src/main/resources</directory><filtering>true</filtering><excludes><!-- 排除字体文件:防止被文本过滤破坏二进制结构 --><exclude>fonts/*</exclude><!-- 排除其他不希望被过滤的二进制文件 --><exclude>*.bin</exclude><exclude>*.dat</exclude><!-- 排除多环境配置文件(由下一个 resource 处理) --><exclude>application-dev.yml</exclude><exclude>application-test.yml</exclude><exclude>application-pro.yml</exclude></excludes></resource><!-- 资源2: 字体文件单独处理,关闭 filtering --><resource><directory>src/main/resources</directory><filtering>false</filtering><includes><include>fonts/*</include></includes></resource><!-- 资源3: 动态加载指定环境的配置文件 --><resource><directory>src/main/resources</directory><filtering>true</filtering><includes><!-- 根据 -DprofileActive=dev 动态包含对应配置 --><include>application-${profileActive}.yml</include></includes></resource></resources><plugins><!-- 其他插件配置 --></plugins>
</build>
✅ 这样 Maven 就不会对字体文件做任何文本处理,确保二进制完整性。
第三步:检查 Java 代码 —— InputStream 是否被正确使用?
即使前两步都正确,代码层面也可能出问题。
❌ 常见错误写法:
InputStream is = getClass().getResourceAsStream("/fonts/msyhl.ttc");
Font font = Font.createFont(Font.TRUETYPE_FONT, is); // ❌ 流可能被提前关闭
Font.createFont() 内部可能异步读取流,而 try-with-resources 会提前关闭流,导致读取不完整。
✅ 正确做法:先读入内存
public static Font loadFont(String fontFileName, int fontType, int fontSize) {try (InputStream is = CarfiCommonUtils.class.getResourceAsStream("/fonts/" + fontFileName)) {if (is != null) {byte[] fontBytes = is.readAllBytes(); // 读入内存try (InputStream bis = new ByteArrayInputStream(fontBytes)) {return Font.createFont(Font.TRUETYPE_FONT, bis).deriveFont(fontType, fontSize);}}} catch (Exception e) {log.error("加载字体失败", e);}return new Font("Arial", fontType, fontSize); // 兜底字体
}
✅ 最佳实践总结
层级 | 措施 | 说明 |
---|---|---|
系统层 | yum install fontconfig freetype | 确保 OpenJDK 字体子系统正常 |
构建层 | 使用 resources保护字体文件 | 防止 Maven 损坏二进制资源 |
代码层 | readAllBytes() + 内存流 | 避免流关闭问题 |
字体格式 | 优先使用 .ttf 而非 .ttc | .ttc 复杂,兼容性差 |
兜底策略 | 提供默认字体(如 Arial) | 防止因字体失败导致服务不可用 |
层级 措施 说明
🧪 推荐:生产环境字体测试工具
编写一个简单的 FontTest.java,用于验证字体是否可加载:
public class FontTest {public static void main(String[] args) {try (InputStream is = FontTest.class.getResourceAsStream("/fonts/msyhl.ttc")) {if (is == null) {System.err.println("❌ 字体文件未找到");return;}byte[] data = is.readAllBytes();System.out.println("📊 字体大小: " + data.length + " bytes");Font font = Font.createFont(Font.TRUETYPE_FONT, new ByteArrayInputStream(data));System.out.println("✅ 字体加载成功: " + font.getFontName());} catch (Exception e) {e.printStackTrace();}}
}
部署前在目标服务器运行此脚本,提前发现问题。
📣 结语
Java 字体加载失败,看似是代码问题,实则可能是 系统依赖缺失 + 构建过程污染 的复合问题。
不要只盯着代码,要从系统、构建、代码三层联动排查。
希望本文能帮你少走弯路,避免在生产环境“抓瞎”。
如果你也在 Docker 中遇到此问题,记得在 Dockerfile 中安装:
RUN apt-get update && apt-get install -y fontconfig
# 或
RUN yum install -y fontconfig freetype