思考这样子的一个场景:
我们需要从一个文件中读取数据,但因为安全的原因想先校验文件的MD5值,遂有以下代码:
1 | FileInputStream input=new FileInputStream("test.config"); |
其中checkMd5定义如下:1
2
3
4
5
6boolean result = false;
if (is != null) {
String newMd5 = SecurityUtil.md5(is);//计算文件的MD5值
result = StringUtil.equals(newMd5, md6);
}
return result;
其中readConfig方法就是直接读取文件内容了。
试问,内容能否正常读出?
答案是不能的。原因很简单,输入流在计算md5时就已经流到底了,当再尝试使用同一个数据输入流去读取文件时,当然就已经读不到数据了。
针对这种情况有三种做法
- 计算完毕重新关闭输入流,再重新打开
- 将文件整个读入到内存
- 标记当前当前输入流位置,计算比赛回到当前位置重新读取
第一种比较直观,但重打开性能上损耗较大,能用,但不推荐;
第二种就不说了,大文件直接懵比;
我想用的是第三种。
InputStream里是支持mark与reset方法的,需要注意的是,这里的mark在理解上可能会有一点问题。从直观上来说,我第一次用是这样子用的
1 | FileInputStream input=new FileInputStream("test.config"); |
字面理解mark方法,应该是指标记当前的位置,然后后续可以通过reset来回到当前的位置,于是将mark的参数设置为0,标记着reset时就回到开始的位置。
自测时感觉还是work的,但一放到生产机上时却有问题。无奈,只得重新去研究这两个方法。
JDK中这两方法的定义如下:
1 | /** |
定义里可以看出,mark的参数并非直观理解上的标记位置,而是指一个容量的概念,翻译如下
在流标记有效时还能读取的字节数
可能第一次看这话会有点绕口,”流标记“跟我们上面理解的是一样的,就是指当前mark的时候流所在的位置;但这个标记不是一旦mark下来就是一直有效的,当流向后读取时,读取的字节数大于readlimit时,这个标记就不能用了,也就是说reset无效了。
在这种理解上去理解上面的问题代码,就很明白了,mark(0)是一点意义都没有的,因为往后一读任何一个字节后,这个mark标记就失效了。
综上,代码应改成
1 | FileInputStream input=new FileInputStream("test.config"); |
mark的readlimit设置为当前可读字节数+1,保证读到尾的时候mark标记都不会失效 。
那拿上面代码来跑就行了吗?结果还是不行的,因为并不是所有的InputStream的子类都是支持mark和reset的,对于FileInputStream而言就是不支持的,一个InputStream子类支持不支持mark,可以通过markSupported来判断:
1 | /** |
所以需要找一个支持mark的inputstream来包装它,如BufferedInputStream。