踩坑InputStream

1. 起因

最近在公司做文件上传的模块,用的是Spring全家桶。文件上传自然会用到什么MultiPartFileInputStream之类的东西,也需要判断文件的类型,传统的做法一般就是根据文件的后缀名去判断文件的类型。但是也有局限性,如果用户把一张图片重命名为无后缀名的文件,那就完犊子了,无法识别文件的类型。

为了解决如上问题呢,查阅了相关的资料,发现各种类型的文件一般都有自己独特的标志。位于整个文件首部,所以可以通过读取文件首部的一段数据来判断文件的类型。听起来真是妙啊,而且HuTool居然自带有这样一个工具类。nice,直接拿来用。

2. 问题来了

先说一下我的大致业务流程:

  • 前端传过来文件,用MultiPartFile接收
  • 拿到FileInputStreamInputStream in = file.getInputStream()
  • 获取文件类型,FileTypeUtil.getType(in)
  • 然后保存到本地,in.transferTo(new FileOutPutStream("D:/xxx/xxx"))

写完了之后我信心满满地开始测试,完美!文件可以上传,也保存到了对应的位置,类型判断也正确(我新建了一个txt文件,然后改成pdf后缀,也能正确识别,真是太妙了)

但是,当我打开保存到本地的文件时,打不开!!!提示文件已损坏,咋回事儿呢,我调试了好久,还是没找到原因所在。就这样过去了一个小时,问题还没解决。

这时,我对比了一下上传前和上传后两个文件,发现了问题。上传后文件居然少了28个字节,这时才恍然大悟。看了下获取文件类型的源码,原来,获取文件类型时读取了这个文件的前28个字节,用来判断文件的类型。然后这个InputStream就从第29个字节处开始transferTo操作。这就导致了所有的文件在上传后文件的类型描述丢失,自然也就打不开了

3. 解决方案

问题找到了,自然就可以对症下药了

这里先介绍一下InputStream这个类,他有两个方法,一个叫mark(),一个叫reset(),故名思意,mark是标记,reset是重置。mark接受一个Integer类型的参数,表示标记的位置,即第几个字节。reset则是重置指针到mark所标志的位置。这两个方法需要配合使用。

然而,InputStream中的mark只有一个空实现,reset调用则直接抛出异常,这点可以通过查看InputStream的源码知道。那咋整呢,其实不难看出,想要使用markreset就需要继承InputStream然后重写这两个方法。自然就是找实现了这两个方法的InputStream子类,一下子就看到了BufferedInputStream,只要把这个InputStream封装成BufferedInputStream不就可以用这两个方法了嘛。

写到这里,解决方案已经很明显了,不就是保存的时候相当于跳过了28个字节嘛,那就在获取文件类型后把指针重新指向文件头不就好了嘛。说干就干,修改后的流程就是下面这样的了。

  • 前端传过来文件,用MultiPartFile接收
  • 拿到BufferedInputStreamBufferedInputStream in = new BufferedInputStream(file.getInputStream())
  • 先打个标记,in.mark(0)
  • 获取文件类型,FileTypeUtil.getType(in)
  • 回到标记点,in.reset()
  • 然后保存到本地,in.transferTo(new FileOutPutStream("D:/xxx/xxx"))

至此,完美解决。