【JAVA基礎】重複使用同一輸入流

博主在工作中遇到問題:需要對讀入的文件 (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 進行校驗,並存入相應的桶中。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章