博主在工作中遇到问题:需要对读入的文件 (MultipartFile) 计算 MD5,同时又需要将其上传到 S3上,即需要对同一输入流进行操作,但是按照流本身所代表的抽象含义,数据一旦流过去,就无法被再次使用。这里给出三种解决的方法:
1. 将输入流转换为文件
这种方式最容易想到,既然需要多次使用,就可以将流转为文件,写入磁盘中,需要的时候再从磁盘读取文件,缺点在于从磁盘写入和读取较为耗时。代码如下:
public void useInputStreamTwiceBySaveToDisk(InputStream inputStream) {
// 文件存放的路径
String desPath = "test001.bin";
try (BufferedInputStream is = new BufferedInputStream(inputStream);
BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(desPath))) {
int len;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
// 需要使用时,通过文件流读取磁盘文件进行
File file = new File(desPath);
StringBuilder sb = new StringBuilder();
try (BufferedInputStream is = new BufferedInputStream(new FileInputStream(file))) {
int len;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
sb.append(new String(buffer, 0, len));
}
System.out.println(sb.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
2. 将输入流转化为数据
可以通过把流中的全部数据读取到一个字节数组中,再通过访问字节数组来获取我们需要的字节信息。例如我们可以利用构建一个 ByteArrayOutputStream 来保留输入流的信息,而需要使用时,通过构造 ByteArrayInputStream 对象来获取相应的 InputStream。代码如下:
public void useInputStreamTwiceSaveToByteArrayOutputStream(InputStream inputStream) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
// 第一次获取 InputStream
InputStream inputStream1 = new ByteArrayInputStream(outputStream.toByteArray());
printInputStreamData(inputStream1);
// 第二次获取 InputStream
InputStream inputStream2 = new ByteArrayInputStream(outputStream.toByteArray());
printInputStreamData(inputStream2);
}
3. 利用输入流的标记与重置
对于 InputStream 类的子类 BufferedInputStream,其在 InputStream 类的基础上提供了内部缓冲区来提升性能,同时提供了对标记和重置的支持。通过在流开始的地方进行标记,当一个接收者读取完流中的内容之后,进行重置即可。重置完成之后,流的当前读取位置又回到了流的开始,就可以再次使用。代码如下:
public void useInputStreamTwiceByUseMarkAndReset(InputStream inputStream) {
StringBuilder sb = new StringBuilder();
try (BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream, 10)) {
byte[] buffer = new byte[1024];
// 调用 mark方法来进行标记
// 这里设置的标记在重置之后允许读取的字节数是整数的最大值
bufferedInputStream.mark(bufferedInputStream.available() + 1);
int len;
while ((len = bufferedInputStream.read(buffer)) != -1) {
sb.append(new String(buffer, 0, len));
}
System.out.println(sb.toString());
// 在第一次调用结束后,显式地调用 reset方法进行流的重置操作
bufferedInputStream.reset();
// 第二次对流进行读取
sb = new StringBuilder();
int len1;
while ((len1 = bufferedInputStream.read(buffer)) != -1) {
sb.append(new String(buffer, 0, len1));
}
System.out.println(sb.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
存在问题
- 对于将输入流转换为文件,缺点显而易见就是需要进行磁盘的读写,影响了速率
- 而对于后面两种,将
输入流转化为数据
还是利用输入流的标记与重置
,两者实际上都是将内容保存到一个byte[]
中。对于输入流转化为数据
,比较明显可以看出其底层是byte[]
,文件的内容将被缓存在这里。而对于利用输入流的标记与重置
,BufferedInputStream 提供了内部缓存区来增加读取速度,而要实现流的重置,也是利用了该缓存区,如果当可重置的范围大于缓存区大小时,继续读入文件时会对缓存区进行扩容,因此,本质上,也是通过利用一个byte[]
进行记录。但是,如果文件内容太大的话,或者服务的内存设定较小时,会导致 GC 频繁,CPU 也将吃紧。
结论
- 如果文件较大,建议还是直接存入磁盘中,虽然磁盘读写占据了部分时间,但是内存相对安全。
- 如果文件有限制大小跟数目,可考虑用后面两种方式,因为是直接存放在内存中,速度更快些。
- 尽量考虑如何避免重复使用读入流,例如我开篇提到的,我需要校验前端传入的 md5 是否正确,同时需要将文件上传到 S3,我最后利用 S3 提供的 api,md5 跟文件流直接传给 S3,由 S3 对 md5 进行校验,并存入相应的桶中。
转载:https://blog.csdn.net/Applying/article/details/104763226
查看评论