踩坑InputStream
踩坑InputStream
1. 起因
最近在公司做文件上传的模块,用的是Spring全家桶。文件上传自然会用到什么MultiPartFile
、InputStream
之类的东西,也需要判断文件的类型,传统的做法一般就是根据文件的后缀名去判断文件的类型。但是也有局限性,如果用户把一张图片重命名为无后缀名的文件,那就完犊子了,无法识别文件的类型。
为了解决如上问题呢,查阅了相关的资料,发现各种类型的文件一般都有自己独特的标志。位于整个文件首部,所以可以通过读取文件首部的一段数据来判断文件的类型。听起来真是妙啊,而且HuTool居然自带有这样一个工具类。nice,直接拿来用。
2. 问题来了
先说一下我的大致业务流程:
- 前端传过来文件,用
MultiPartFile
接收 - 拿到
FileInputStream
,InputStream 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
的源码知道。那咋整呢,其实不难看出,想要使用mark
和reset
就需要继承InputStream
然后重写这两个方法。自然就是找实现了这两个方法的InputStream
子类,一下子就看到了BufferedInputStream
,只要把这个InputStream
封装成BufferedInputStream
不就可以用这两个方法了嘛。
写到这里,解决方案已经很明显了,不就是保存的时候相当于跳过了28个字节嘛,那就在获取文件类型后把指针重新指向文件头不就好了嘛。说干就干,修改后的流程就是下面这样的了。
- 前端传过来文件,用
MultiPartFile
接收 - 拿到
BufferedInputStream
,BufferedInputStream in = new BufferedInputStream(file.getInputStream())
- 先打个标记,
in.mark(0)
- 获取文件类型,
FileTypeUtil.getType(in)
- 回到标记点,
in.reset()
- 然后保存到本地,
in.transferTo(new FileOutPutStream("D:/xxx/xxx"))
至此,完美解决。