本文目的是爲了記錄,項目開發時的一個小BUG,如果你是大佬,或者對InputStream十分熟悉,那麼可以忽略!
今天開發項目的時候遇見了一個小BUG,該功能如下:
- 讀取指定FTP服務器裏面的文件數據,並計算md5簽名
- 推送到備份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;
}
我們看到,他將該數組記錄下來,而且還額外的初始化了pos
和count
屬性
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();
}
這個問題,不是一個大問題,但是卻是平常開發中需要注意的小細節!遇見此類錯誤時,不要心急,一步一步找總能解決問題的!
才疏學淺,如果文章中理解有誤,歡迎大佬們私聊指正!歡迎關注作者的公衆號,一起進步,一起學習!