這個星期公司的項目接口進行改造,公司的接口有的采用了WebService的方式,有的使用的是Http協議+Servlet的形式,對於WebService的形式還真沒有接觸過,閑着沒事的時候學習一下,畢竟新接口都采用這種方式,也是一種趨勢。在改造Http協議+Servlet的接口過程中對Http協議和Servlet又有了一個新的認識,特別是Http協議,以前腦子里亂亂的,知道有這個東西,知道它是做什么的,很模糊,做web開發的還是需要深刻理解Http協議的。在接口改造的過程中需要Servlet讀取客戶端提交過來的XML,所以用到了Request.getInputStream API,碰到的問題是以前的舊接口中寫了兩次Request.getInputStream(),最后一次讀取的內容為空,雖然為空,但是沒有影響接口的使用,所以一直放在這里沒有人管,今天發現了研究了一下原因,也能把沒用的代碼刪除掉。
為什么第二次在Servlet中獲取InputStream的值為空,讀取不到XML內容,這個問題要復習一下java中IO的知識了,在java中讀取一個文件或者字符串的內容的代碼大家都會寫,下邊是使用ByteArrayInputStream和ByteArrayOutputStream進行演示:
1 @Test 2 public void testByteArrayInputStream() throws Exception { 3 4 String str = "AAAAACCCCcCBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; 5 //ByteArrayInputStream是把一個byte數組轉換成一個字節流,讀取的內容是從byte數組中讀取的 6 ByteArrayInputStream byteInputStream = new ByteArrayInputStream(str.getBytes()); 7 8 //ByteArrayOutputStream生成對象的時候,是生成一個100大小的byte的緩沖區,寫入的時候,是把內容寫入內存中的一個緩沖區 9 ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(100); 10 11 int i =0; 12 byte [] b = new byte[100]; 13 while((i = byteInputStream.read(b))!= -1){ 14 byteOutput.write(b, 0, i); 15 } 16 System.out.println(new String(byteOutput.toByteArray())); 17 18 }
打印結果是:AAAAACCCCcCBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
把一個String字符串的內容使用ByteArrayOutputStream讀取出來,然后打印顯示。這個代碼沒有什么問題,估計大家都能寫出來,但是看一下下邊添加一行代碼之后的內容:
1 @Test 2 public void testByteArrayInputStream() throws Exception { 3 String str = "AAAAACCCCcCBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; 4 //ByteArrayInputStream是把一個byte數組轉換成一個字節流,讀取的內容是從byte數組中讀取的 5 ByteArrayInputStream byteInputStream = new ByteArrayInputStream(str.getBytes()); 6 7 //調用這個方法,會影響到下次讀取,下次再調用這個方法,讀取的起始點會后移5個byte 8 byteInputStream.read(new byte[5]); 9 10 //ByteArrayOutputStream生成對象的時候,是生成一個100大小的byte的緩沖區,寫入的時候,是把內容寫入內存中的一個緩沖區 11 ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(100); 12 13 int i =0; 14 byte [] b = new byte[100]; 15 while((i = byteInputStream.read(b))!= -1){ 16 byteOutput.write(b, 0, i); 17 } 18 System.out.println(new String(byteOutput.toByteArray())); 19 }
打印結果是:CCCCcCBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
我在第8行添加了一行代碼,這行代碼可以把String生成的byte數組中讀取5個字節的內容,這行代碼會影響到后邊第15行byteInputStream的讀取結果,顯然”AAAAA“在輸出中沒有了,這是為什么呢?我感覺需要查看java中ByteArrayInputStream的Read方法的實現源碼,查看的結果其實可以總結一個句話,就是在InputStream讀取的時候,會有一個pos指針,他指示每次讀取之后下一次要讀取的起始位置,在API文檔中是這樣解釋的:(看過InputStream源碼的都明白,read方法其實調用的都是帶有三個參數的方法)
1 public int read(byte[] b,int off, int len) 2 Reads up to len bytes of data into an array of bytes from this input stream. If pos equals count, then -1 is returned to indicate end of file. Otherwise, the number k of bytes read is equal to the smaller of len and count-pos. If k is positive, then bytes buf[pos] through buf[pos+k-1] are copied into b[off] through b[off+k-1] in the manner performed by System.arraycopy. The value k is added into pos and k is returned.
就是在每次讀取的時候會更新pos的值,當你下次再來讀取的時候是從pos的位置開始的,而不是從頭開始,所以第二次獲取String中的值的時候是不全的,”AAAAA“丟掉了,這也就導致了兩次調用request.getInputStream,第二次的時候肯定獲取不了值,因為第一次讀取完成之后pos指針在末尾,下次再讀取肯定讀取不到,同request.getInputStream兩次調用返回的對象是同一個對象。讀取的是同一個Stream。
但是仔細查看API文檔你會發現有這樣一個方法:
public void reset() Resets the buffer to the marked position. The marked position is 0 unless another position was marked or an offset was specified in the constructor.
就是可以把pos的指針的位置重置為起始位置,但是調用它是有條件的,不是所有的IO讀取流都能調用這個方法.看一下有這個方法
public boolean markSupported() Tests if this input stream supports the mark and reset methods. Whether or not mark and reset are supported is an invariant property of a particular input stream instance. The markSupported method of InputStream returns false.
這個方法可以判斷是不是支持reset()方法的調用,我也沒有試servlet的InputStream是否可以調用,直接查看了一下Servlet的源碼。
request.getInputStream返回的其實ServletInputStream,查看一下源碼你會發現:ServletInputStream繼承了InputStream同時沒有重寫reset()方法,查看一下InputStream源碼:
InputStream的reset()方法源碼是這樣的:
1 public synchronized void reset() throws IOException { 2 throw new IOException("mark/reset not supported"); 3 }
調用reset方法直接拋出異常,所以ServletInputStream是不能調用reset方法,這就導致了只能調用一次getInputStream(),第二次調用的時候沒有辦法獲取到InputStream流中的原因。現在我們更改一下上邊讀取String字符串的例子:
1 @Test 2 public void testByteArrayInputStream() throws Exception { 3 String str = "AAAAACCCCcCBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; 4 //ByteArrayInputStream是把一個byte數組轉換成一個字節流,讀取的內容是從byte數組中讀取的 5 ByteArrayInputStream byteInputStream = new ByteArrayInputStream(str.getBytes()); 6 7 //調用這個方法,會影響到下次讀取,下次再調用這個方法,讀取的起始點會后移5個byte 8 byteInputStream.read(new byte[5]); 9 byteInputStream.reset();//調用reset方法可以使read中的pos指針復位 10 11 12 //ByteArrayOutputStream生成對象的時候,是生成一個100大小的byte的緩沖區,寫入的時候,是把內容寫入內存中的一個緩沖區 13 ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(100); 14 15 int i =0; 16 byte [] b = new byte[100]; 17 while((i = byteInputStream.read(b))!= -1){ 18 byteOutput.write(b, 0, i); 19 } 20 System.out.println(new String(byteOutput.toByteArray())); 21 }
打印結果是:AAAAACCCCcCBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
在第8行添加一行代碼,byteInputStream調用reset()方法,打印的結果回復了正常,我們看一下ByteInputStream中reset()的源碼:
1 /** 2 * Resets the buffer to the marked position. The marked position 3 * is 0 unless another position was marked or an offset was specified 4 * in the constructor. 5 */ 6 public synchronized void reset() { 7 pos = mark; 8 }
這一次沒有拋出異常,而是把mark的值賦值給pos了,mark的值為0,所以在調用reset()方法之后可以從頭開始讀取。
進過查看JDK的源碼分析為什么Servlet中讀取InputSteam只能讀取一次的問題,果斷把沒用的代碼刪除了,只有明白原理明白為什么,在寫代碼的時候才能很肯定的明白自己寫的代碼一定是正確的或者是錯誤的,不用再去運行一下試試。
以后閑着沒事多看JDK的源碼,看源碼的確是一個很好的學習的過程,到此結束。