记一次java创建字体时错误提示:java.io.IOException: Problem reading font data.
java创建Font对象时,报java.io.IOException: Problem reading font data.异常
前情
本地开发时,需要将文字作为水印加到图片中,使用的是之前其他项目写好的工具类,其中用了字体,并解决了部署到Linux(其实是docker)环境下提示找不到字体的问题:
将字体文件放到项目的resources文件夹下,程序中通过读取该文件的方式,摆脱依赖系统字体问题来解决:
/**
1. 加载本地字体
2. @param fontSize 字体大小
3. @param bold 是否加粗
4. @param fontURL 字体文件路径
5. @return 字体对象
*/
public static Font loadLocalFont(int fontSize, boolean bold, String fontURL) {
try (InputStream fontStream = new cn.hutool.core.io.resource.ClassPathResource(fontURL).getStream()){
Font font = Font.createFont(Font.TRUETYPE_FONT, fontStream);
return font.deriveFont(bold ? Font.BOLD : Font.PLAIN,fontSize);
} catch (FontFormatException | IOException e) {
log.error("创建字体失败", e);
throw new ServiceException("创建字体失败");
}
}
上述代码中,fontURL默认使用的是居于resources.font文件夹下的字体文件,值为"/font/simhei.ttf"。
其中获取InputStream的方法还有:
- 如上,通过使用hutool包中的 ClassPathResource(fontURL).getStream() 方法获取
- 通过springframework的 ClassPathResource(fontURL).getInputStream() 方法获取(完整路径org.springframework.core.io.ClassPathResource)
- 通过 ClassLoader(类加载器)获取:
// 非静态方法中 InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(fontURL); // 静态方法中,可将String换为当前类 InputStream inputStream = String.class.getClassLoader().getResourceAsStream(fontURL);
其实底层都是通过第三种方式来实现的
同时,第三种方法要注意,有的人使用的是下面的方式:
// 非静态方法中
InputStream inputStream = this.getClass().getResourceAsStream(fontURL);
// 静态方法中,可将String换为当前类
InputStream inputStream = String.class.getResourceAsStream(fontURL);
这样是无法获取到文件流的,区别在于通过 类 获取还是通过 类加载器 获取
问题记录
本地开发过程中没有任何问题,能正常创建字体,发布到docker环境时就报错:
java.io.IOException: Problem reading font data.
at java.desktop/java.awt.Font.createFont0(Font.java:1206)
at java.desktop/java.awt.Font.createFont(Font.java:1075)
....
一开始很疑惑,之前使用的项目都没有出现这种问题,于是找了很多关于Problem reading font data.资料,基本给出的解决方案都是因为Linux环境没有字体,要么将字体加到环境中(docker中使用这种方式肯定不靠谱),要么使用上面给的方式将字体问价放到项目中。然后想到,会不会是因为这个流无效后者流类型导致的:
public static Font createFont(int fontFormat, InputStream fontStream)
throws java.awt.FontFormatException, java.io.IOException {
if (hasTempPermission()) {
return createFont0(fontFormat, fontStream, false, null)[0];
}
// Otherwise, be extra conscious of pending temp file creation and
// resourcefully handle the temp file resources, among other things.
CreatedFontTracker tracker = CreatedFontTracker.getTracker();
boolean acquired = false;
try {
acquired = tracker.acquirePermit();
if (!acquired) {
throw new IOException("Timed out waiting for resources.");
}
return createFont0(fontFormat, fontStream, false, tracker)[0];
} catch (InterruptedException e) {
throw new IOException("Problem reading font data.");
} finally {
if (acquired) {
tracker.releasePermit();
}
}
}
注意源码中的 throw new IOException(“Problem reading font data.”); 它主动抛出了Problem reading font data异常,于是将获取到的InputStream对象输出来,不出意外的出现了意外:本地环境获取到的是java.io.BufferedInputStream,但docker环境下获取到的是一个Zip…的流,心中一喜,可能是这个原因,于是,做了个判断,如果获取到的流不是java.io.BufferedInputStream,就把这个流转换为java.io.BufferedInputStream:
inputStream = new java.io.BufferedInputStream(inputStream);// 次写法不严谨,因为有个流没有关闭
然而,还是包Problem reading font data.错误。
于是,继续找问题。
想到之前写这个方法的时候考虑将这个流进行复用时出现过问题,因为这个流只能被消费一次,会不会是这个流有问题,于是输出了流的available值,若为0,这代表这个流确实有问题;然鹅,现实是,流没有问题,无论是转换前还是转换后,available都不为零。
于是,继续……
看到有博主说是因为openjdk有些内容因为开源的缘故,被删除或者改写过,比如openjdk中没有字体组件,需要手动安装;遂手动尝试dockerfile中添加字体组件服务
FROM openjdk:17-jdk-alpine
WORKDIR /app
ARG JAR_FILE=*.jar
COPY ${JAR_FILE} application.jar
# 安装 fontconfig 和 ttf-dejavu字体
RUN apk add --update ttf-dejavu fontconfig
&& apk add fontconfig \
&& apk add --update ttf-dejavu \
&& fc-cache --force
EXPOSE 6088
ENTRYPOINT ["java", "-Dspring.profiles.active=dev", "-jar", "application.jar"]
嗯……问题解决,可以了。
但是…… 是的,还有个但是:
这个镜像构建用了20多分钟,是的,构建docker镜像就用了20多分钟
安装字体组件就1,397秒,折合23+分钟,这个肯定是不能接受的。
当然,本着严谨的作风,可能是jdk17获取jar中文件的底层实现有变动,将字体文件放到了镜像文件中,通过读取本地文件的方式获取文件流,当然结果还是报Problem reading font data.
那就很明确问题了——缺少字体组件,解决办法也就有了——要么解决字体组件安装速度问题,要么解决字体组件缺失问题。
组件缺失是因为jdk导致的,好像没法处理;尝试了各种加快字体组件安装的方式,但是,只能解决下载字体组件的问题,但是安装依然是个问题:
FROM openjdk:17-jdk-alpine
WORKDIR /app
ARG JAR_FILE=*.jar
COPY ${JAR_FILE} application.jar
# 安装 fontconfig 和 ttf-dejavu字体
RUN echo -e "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.4/main\n \
https://mirror.tuna.tsinghua.edu.cn/alpine/v3.4/community" > /etc/apk/repositories
# 此处必须分为两层构建,不能使用 && 方式组合命令一层构建完成
RUN apk add --update ttf-dejavu fontconfig
EXPOSE 6088
ENTRYPOINT ["java", "-Dspring.profiles.active=dev", "-jar", "application.jar"]
这样解决了下载问题,但是安装依然很慢,难以接受:总构建168秒,安装就用字体组件就用了162秒,完全不能接受的速度,并且构建了两层,也不科学。
好了,问题还是没解决。既然安装组件行不通,那看来只有回到组件缺失问题上去了。
回头看了下之前发布正常使用的dockerfile,记忆中使用的是jdk8,但是仔细比较发现使用的基础镜像好像有点不太一样:
老项目的基础镜像
FROM openjdk:8-jre
...
基础镜像好像多了个alpine,于是查下了这个alpine是个啥,说是从jdk8以后,没有jre镜像了,为了精简镜像,于是有了alpine,用来替代缺少的jre(个人猜测,但是没有查到官方的说法),到这就能说通了,为什么会缺少字体组件了:JDK与JRE的区别,想必大家都清楚了。但是,该怎么解决问题?目前又不能访问docker hub,使用 docker search命令查到的东西其实意义不大。于是大胆尝试了一波,拉取了一下openjdk17:
docker image pull openjdk:17-jdk
嚯~~~好家伙,竟然有这个镜像。毫不迟疑,使用这个镜像作为基础镜像构建docker:
FROM openjdk:17-jdk
WORKDIR /app
ARG JAR_FILE=*.jar
COPY ${JAR_FILE} application.jar
EXPOSE 6088
ENTRYPOINT ["java", "-Dspring.profiles.active=dev", "-jar", "application.jar"]
然后很丝滑的解决了问题。
总结与小记
原来根本问题是运行环境中缺少了组件,不要轻易使用alpine作为基础镜像,毕竟那只是类似一个JRE环境。
然后不要懒惰,多尝试,openjdk17-alpine是之前同事构建的时候使用的,也没多想,无脑使用。多问个为什么,果然是有意外收获的。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)