Java中InputStream read()一次不当使用导致的bug
2023-05-24

背景

今天有个需求是需要使用到Apache的poi包去读取word文件。也就是doc和docx文件。我在引入poi之后,自己封装了一个DocExtractor类,里面组合了XWPFDocument用来读取解析docx文件。

我按照正常流程封装,写测试用例。结果测试报错如下:

1
org.apache.poi.openxml4j.exceptions.NotOfficeXmlFileException: No valid entries or contents found, this is not a valid OOXML (Office Open XML) file

https://img.hanjiawei.com/i/2023/05/24/646e137105de5.png

问题解决过程

起初看到这个错误,我第一时间以为是文件格式的问题,在网上搜索到的解决方案也大都是文件格式有问题,比如文件加密、doc文件直接修改后缀为docx文件、文件损坏等。

刚开始我是坚定不移的认为这是文件格式的问题,那我就先按照文件格式有问题的思路排查了一番,包括自己重新创建一个新的docx文件、用别人电脑创建等。最后发现即使我能确定我的docx文件是正确的,代码还是报错。

初见猫腻

在排除掉文件格式的问题之后,我就仔细排查代码,我的代码大概如下:

1
2
3
4
5
6
public DocExtractor(InputStream inputStream) throws IOException {

// 根据文件头信息,获取文件类型
String type = FileUtil.getFileType(inputStream);
XWPFDocument xwpfDocument = new XWPFDocument(inputStream);
}

报错就在合格构造方法里。我就debug到这个里,一步一步看,发现inputstream在查询完他的文件类型之后,对象属性有些变化。

查询前
https://img.hanjiawei.com/i/2023/05/24/646e15ca584bf.png

查询后

https://img.hanjiawei.com/i/2023/05/24/646e15dca54f3.png

那就猜测是因为这几个变量变化的原因导致poi拿到了修改后的InputStream 进而导致创建XWPFDocument失败。将这行代码去掉,果然发现成功创建,也不报错了。

那么问题就出在FileUtil.getFileType(inputStream)里。

分析原因

为什么获取文件的类型会导致InputStream流发生变化呢?

我在网上查了一下,原因如下:

InputStream的读取是单向的,也就是说读取顺序是按照文件中的数据存储书序来的。另外,通过.read()方法读出来的数据是个临时变量,java会自动在堆中为其分配一个内存空间,但是当.read()方法执行结束,垃圾回收器会立刻将其删除,因此在程序中.read(byte[] bytes)方法中的bytes参数才是实际上是用来存储读取出来数据的参数。如果文件保存了10个字节的数据,而bytes长度为8,那么inputStream会按照每8个字节读一次文件。

在我的代码中,查询文件类型本身就调用了InputStream的read()方法。这就导致获取完文件类型后的InputStream是已经读取过的,进而导致构造XWPFDocument对象时,poi读取不到完整的文件,所以报错this is not a valid OOXML

问题解决

找到原因了那问题解决就简单了,可以在获取文件头信息之前借助ByteArrayInputStreamInputStream复制一份。

这个问题如果对IO流比较熟悉的人确实是很容易就能定位出这个问题。

我就对IO流的这个单向读的细节不清楚,在工作中重复读取流的场景确实这也是第一次遇见。吃一堑,长一智。有收获就是成长。

再附上ChannelInputStream中bb bs b1这几个成员变量的作用:

  • <br class="Apple-interchange-newline"/>bb 是一个 ByteBuffer 对象,用于读取数据到其中。
  • bs 是一个 byte 数组,表示读取数据的缓冲区。
  • b1 是一个 byte,表示读取到的单个字节。