背景
今天有个需求是需要使用到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 |
问题解决过程
起初看到这个错误,我第一时间以为是文件格式的问题,在网上搜索到的解决方案也大都是文件格式有问题,比如文件加密、doc文件直接修改后缀为docx文件、文件损坏等。
刚开始我是坚定不移的认为这是文件格式的问题,那我就先按照文件格式有问题的思路排查了一番,包括自己重新创建一个新的docx文件、用别人电脑创建等。最后发现即使我能确定我的docx文件是正确的,代码还是报错。
初见猫腻
在排除掉文件格式的问题之后,我就仔细排查代码,我的代码大概如下:
1 | public DocExtractor(InputStream inputStream) throws IOException { |
报错就在合格构造方法里。我就debug到这个里,一步一步看,发现inputstream在查询完他的文件类型之后,对象属性有些变化。
查询前
查询后
那就猜测是因为这几个变量变化的原因导致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。
问题解决
找到原因了那问题解决就简单了,可以在获取文件头信息之前借助ByteArrayInputStream将InputStream复制一份。
这个问题如果对IO流比较熟悉的人确实是很容易就能定位出这个问题。
我就对IO流的这个单向读的细节不清楚,在工作中重复读取流的场景确实这也是第一次遇见。吃一堑,长一智。有收获就是成长。
再附上ChannelInputStream中bb bs b1这几个成员变量的作用:
<br class="Apple-interchange-newline"/>bb是一个ByteBuffer对象,用于读取数据到其中。bs是一个byte数组,表示读取数据的缓冲区。b1是一个byte,表示读取到的单个字节。


