天啦嚕,項目上使用InputStream,我被坑了一把!

本文目的是爲了記錄,項目開發時的一個小BUG,如果你是大佬,或者對InputStream十分熟悉,那麼可以忽略!

今天開發項目的時候遇見了一個小BUG,該功能如下:

  1. 讀取指定FTP服務器裏面的文件數據,並計算md5簽名
  2. 推送到備份FTP服務器

以上功能涉及到公司項目功能實現,不能詳細多說,大致功能就是這樣,讀取一個MD5同步到另外一個服務器,期間我遇到什麼問題了呢?先看一個模擬的代碼實現!

package com.inputstreams;

import org.apache.commons.codec.digest.DigestUtils;

import java.io.ByteArrayInputStream;
import java.io.IOException;

/**
 * @author huangfu
 */
public class TestInputStream {

    public static void main(String[] args) throws IOException {
        //從源FTP服務器獲取一個InputStream流信息
        ByteArrayInputStream byteArrayInputStream = 
            						new ByteArrayInputStream("12312312312".getBytes());

        byte[] bytes = new byte[1024];
        int index = 0;
        //計算MD5值
        String fileMd5 = DigestUtils.md5Hex(byteArrayInputStream);
        System.out.println(fileMd5);

        //模擬推送方法讀取InputStream
        while ((index = byteArrayInputStream.read(bytes)) != -1) {
            System.out.println("業務操作"+index);
            //。。。。業務操作
        }

        byteArrayInputStream.close();
    }
}

結果:

在這裏插入圖片描述

該代碼的結果如圖所示,MD5被計算了出來,但是卻沒有打印業務代碼!

項目的的最後結果也是我在FTP服務器上看到了,我同步的文件,就認爲我同步上去了,也就沒有管他!中午喫完飯,無聊期間,在目標FTP服務器上執行cat xxx.txt命令,驚奇的發現,裏面居然沒有內容,這引起了我極大的好奇,一開始我認爲是我在源FTP服務器上壓根就沒獲取到InputStream流信息,所以推送的時候沒有推送上去!

但是經過Debug後發現,MD5值被完整的算了出來,這就證明了一點Input流是有值的,事實證明,確實是有值的!

於是我進入到獲取MD5值方法的源碼裏面看:

public static MessageDigest updateDigest(final MessageDigest digest, 
                                   final InputStream inputStream)throws IOException {
    //構建一個字節數組
    final byte[] buffer = new byte[STREAM_BUFFER_LENGTH];
    int read = inputStream.read(buffer, 0, STREAM_BUFFER_LENGTH);
	//循環讀取流裏面的數據,放入到byte數組,返回!
    while (read > -1) {
        digest.update(buffer, 0, read);
        read = inputStream.read(buffer, 0, STREAM_BUFFER_LENGTH);
    }

    return digest;
}

原來,我調用的方法是基於字節數組來計算的,此時我突然想到,NIO的ByteBuffer在讀取數據時,是由一個指針的概念的,每次讀取一個數據,指針都會後移,直到與緩衝區長度重疊爲止,再想重複讀取,就需要調用reset()方法,來恢復指針位置,那麼InputStream是不是這樣呢,我不禁進入他的源碼查看,果然不出我所料我看到ByteArrayInputStream裏面的read(byte b[], int off, int len)方法有這樣一段邏輯:

1. 構建ByteArrayInputStream時:

public ByteArrayInputStream(byte buf[]) {
    this.buf = buf;
    this.pos = 0;
    this.count = buf.length;
}

我們看到,他將該數組記錄下來,而且還額外的初始化了poscount屬性

2.讀取數據時:

public synchronized int read(byte b[], int off, int len) {
    //檢查參數
    if (b == null) {
        throw new NullPointerException();
    } else if (off < 0 || len < 0 || len > b.length - off) {
        throw new IndexOutOfBoundsException();
    }
	
    //★檢查當前指針位置是否大於數據長度,是的話就證明已經讀取完畢,返回-1
    if (pos >= count) {
        return -1;
    }

    //計算有效數據也就是可讀數據長度
    int avail = count - pos;
    //如果給定讀取的長度尚有冗餘,那麼強制賦值爲有效長度,避免空間的浪費
    if (len > avail) {
        len = avail;
    }
    //若讀取長度小於0,那麼直接不讀取
    if (len <= 0) {
        return 0;
    }
    //執行數組的複製,將初始化時傳遞的數組,根據設置的長度複製到給定的字節數組!
    //將 buf 從pos指向的位置開始 複製到 b數組的off位置  複製 len個字節
    System.arraycopy(buf, pos, b, off, len);
    //將 pos指針指向下一次將要讀取的位置
    pos += len;
    return len;
}

關鍵點就在pos指針身上,他決定着數據到底能被複制多少,想到此我不僅恍然大悟,原來在進行md5計算的時候,計算md5的方法會讀取一遍,導致pos指針後移到最後一位,指針位置與長度相同,導致上述代碼標星★位置判斷成立,返回-1,最終導致了,第二次讀取的時候,因爲返回長度爲-1就不讀取了!

看到這裏茅塞頓開,突然回想到ByteBuffer中是存在一個恢復指針的方法的,那麼在ByteArrayInputStream中是否也存在一個類似的方法呢?大概看了一下源碼,果然讓我找到了:

public synchronized void reset() {
    //將指針位置恢復到標記位置
    pos = mark;
}

什麼是標記位置呢?ByteArrayInputStream爲了記錄一次實例讀取中的初始位置,故而增加的方法,mark屬性默認爲 0 代碼如下:

protected int mark = 0;

當然,不是一直爲0 ,當我們在構造ByteArrayInputStream對象時,指定了初始位置,那麼mark屬性也會隨之改變:

public ByteArrayInputStream(byte buf[], int offset, int length) {
    this.buf = buf;
    this.pos = offset;
    this.count = Math.min(offset + length, buf.length);
    this.mark = offset;
}

好了,最終問題圓滿解決,最終的使用方式爲:

public static void main(String[] args) throws IOException {
    //從源FTP服務器獲取一個InputStream流信息
    ByteArrayInputStream byteArrayInputStream = 
        							new ByteArrayInputStream("12312312312".getBytes());

    byte[] bytes = new byte[1024];
    int index = 0;
    //計算MD5值
    String fileMd5 = DigestUtils.md5Hex(byteArrayInputStream);
    //★ 重置讀指針位置,方便複用
    byteArrayInputStream.reset();
    System.out.println(fileMd5);

    //模擬推送方法讀取InputStream
    while ((index = byteArrayInputStream.read(bytes)) != -1) {
        System.out.println("業務操作"+index);
        //。。。。業務操作
    }

    byteArrayInputStream.close();
}

這個問題,不是一個大問題,但是卻是平常開發中需要注意的小細節!遇見此類錯誤時,不要心急,一步一步找總能解決問題的!


才疏學淺,如果文章中理解有誤,歡迎大佬們私聊指正!歡迎關注作者的公衆號,一起進步,一起學習!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章