记一次java创建字体时错误提示: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的方法还有:

  1. 如上,通过使用hutool包中的 ClassPathResource(fontURL).getStream() 方法获取
  2. 通过springframework的 ClassPathResource(fontURL).getInputStream() 方法获取(完整路径org.springframework.core.io.ClassPathResource)
  3. 通过 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是之前同事构建的时候使用的,也没多想,无脑使用。多问个为什么,果然是有意外收获的。