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

JAVA旭陽 發表於 2022-12-08

前言

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

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銀行專案上,報了下面的一個錯誤資訊,陣列越界,如下圖所示:

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

  1. 反編譯jar包的程式碼,在如下位置用到了陣列讀取,根據=號切割為組數,如下圖所示:

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

  1. 而這個切割的字串,是呼叫loadResource方法載入ORG_PATH_MAP得到,如下圖所示:

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

  1. 我們再來看下loadResource的程式碼:

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

  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試了一下:

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

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

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

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

小結: 在讀物流時呼叫的是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讀取,出現問題。

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

總結

  1. 在程式碼編寫過程中,available()方法僅用於估算接收資料的總長度或資料塊的長度,不要用於任何需要準確計算的場合,更不要用於開闢一個可以剛好容納所有資料的緩衝區。
  2. 對於呼叫InputStream.read(…),務必進行迴圈呼叫,直至返回-1,無論輸入資料來源是網路資料還是本地檔案。

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

如果本文對你有幫助的話,請留下一個贊吧
更多技術幹活和學習資料盡在個人公眾號——JAVA旭陽
記一次InputStream流讀取不完整留下的慘痛教訓