記一次InputStream流讀取不完整留下的慘痛教訓

java旭陽 發佈 2022-12-08T10:35:20.264532+00:00

前言首先,問問大家下面這段流讀取的代碼是否存在問題呢?inputStream = ....try { // 根據inputStream的長度創建字節數組 byte[] arrayOfByte = new byte[inputStream.

前言

首先,問問大家下面這段流讀取的代碼是否存在問題呢?

inputStream = ....
try {
    // 根據inputStream的長度創建字節數組
    byte[] arrayOfByte = new byte[inputStream.available()];
    // 調用read 讀取字節數組
    inputStream.read(arrayOfByte, 0, arrayOfByte.length);
    return new String(arrayOfByte);
}catch (Exception e){
    e.printStackTrace();
}
複製代碼

實際上的確是有問題的,而且在線上環境結結實實的坑了我們一把。

問題回溯

  1. 在xx銀行項目上,報了下面的一個錯誤信息,數組越界,如下圖所示:
  1. 反編譯jar包的代碼,在如下位置用到了數組讀取,根據=號切割為組數,如下圖所示:
  1. 而這個切割的字符串,是調用loadResource方法加載ORG_PATH_MAP得到,如下圖所示:
  1. 我們再來看下loadResource的代碼:
  1. 這裡的是加載ORG_PATH_MAP.class文件的內容,這個文件雖然class,但是裡面存儲內容的格式如下:
zj=浙江分公司,sh=上海分公司,fz=福州分公司
複製代碼

在我們多次確認數據格式也沒有問題以後,就陷入了沉思,大家有發現什麼問題呢?

原因分析

我們就懷疑讀取的時候是不是有問題,是不是讀取得不完整導致得。

我們看了下InputStream類的javadoc:

  1. available()

返回可以從此輸入流讀取(或跳過)的字節數的估計值 ,返回的不是整個數據的長度, 是這次read可讀的長度。

InputStream的不同子類對InputStream.available()可能會有不同的實現,一些實現會返回當前可一次無阻塞讀入的字節數,另一些實現會返回這個輸入流可讀入的字節總數, 因此應儘量避免使用該返回值作為開闢能容納該輸入流所有數據的緩衝大小依據。

  1. int read(byte b[], int off, int len)

從輸入流中讀取最多len字節的數據到字節數組中。嘗試讀取最多len字節,但可能會讀取更小的數字。實際讀取的字節數以整數形式返回。

所以做了一個demo試了一下:

  • 有問題的這個項目是用AppClassLoader加載當前路徑下的類,可以發現InputStream的實現類是JarURLInputStream

運行結果如下圖,可能確實發現讀少了。

小結: 在讀物流時調用的是available方法,點擊進入其源碼發現其返回的是當前流可用長度(估計值),不是流的總長度。而在read方法讀取流中數據到buffer中,但讀取長度為1至buffer.length,若流結束或遇到異常則返回-1。也就是說當實際文件的長度超過此估計可用長度時也不會繼續讀,而是結束讀取。從而導致讀取的流並不完整。這很大程度取決於不同的實現。

解決方案

方案一:

 public static byte[] streamToByteArray(InputStream in) throws IOException {
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int n;
        while (-1 != (n = in.read(buffer))) {
            output.write(buffer, 0, n);
        }
        return output.toByteArray();
    }
複製代碼

藉助ByteArrayOutputStream,通過循環去讀取流,直到讀取完成,如果返回-1,表示全部讀取完成。

方案二:

public static byte[] streamToByteArray(InputStream in) throws IOException {
        byte[] bytes = new byte[bufferlength];
        BufferedInputStream bis = new BufferedInputStream(is);
        int length = bis.read(bytes, 0, bufferlength)
        return bytes;
    }
複製代碼

採用BufferedInputStream,它底層其實也是循環讀取。

為什麼測試沒發現?

實際情況是我們這是一個公共jar,被不同的組件下載,有的組件放到classpath下通過AppClassloader加載,有的組件通過自定義的classLoader加載,開發測試我們都是用的自定義DynamicClassloader加載,它的InputStream的實現類是ByteInputStream,是沒有發現問題的。

而本次是另外一個spark組件, 他們把jar 放到了classpath下 也就是用AppClassloader,最終用了JarURLInputStream讀取,出現問題。

總結

  1. 在代碼編寫過程中,available()方法僅用於估算接收數據的總長度或數據塊的長度,不要用於任何需要準確計算的場合,更不要用於開闢一個可以剛好容納所有數據的緩衝區。
  2. 對於調用InputStream.read(…),務必進行循環調用,直至返回-1,無論輸入數據源是網絡數據還是本地文件。

在平時的開發過程中,還是需要注重細節,不然會出現意料不到的問題。

關鍵字: