博主在工作中遇到問題:需要對讀入的文件 (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 進行校驗,並存入相應的桶中。