Apache Common fileUpload API 詳解
文件上傳組件的應(yīng)用與編寫在許多Web站點應(yīng)用中都需要為用戶提供通過瀏覽器上傳文檔資料的功能,例如,上傳郵件附件、個人相片、共享資料等。對文件上傳功能,在
瀏覽器端提供了較好的支持,只要將FORM表單的enctype屬性設(shè)置為“multipart/form-data”即可;但在Web服務(wù)器端如何獲取瀏覽器上傳的文
件,需要進行復(fù)雜的編程處理。為了簡化和幫助Web開發(fā)人員接收瀏覽器上傳的文件,一些公司和組織專門開發(fā)了文件上傳組件。本章將詳細介
紹如何使用Apache文件上傳組件,以及分析該組件源程序的設(shè)計思路和實現(xiàn)方法。
1.1 準備實驗環(huán)境
按下面的步驟為本章的例子程序建立運行環(huán)境:
(1)在Tomcat 5.5.12的<tomcat的安裝目錄>\webapps目錄中創(chuàng)建一個名為fileupload的子目錄,并在fileupload目錄中創(chuàng)建一個名為test.html
的網(wǎng)頁文件,在該文件中寫上“這是test.html頁面的原始內(nèi)容!”這幾個字符。
(2)在<tomcat的安裝目錄>\webapps\fileupload目錄中創(chuàng)建一個名為WEB-INF的子目錄,在WEB-INF目錄中創(chuàng)建一個名為classes的子目錄和一個
web.xml文件,web.xml文件內(nèi)容如下:
<web-app>
</web-app>
(3)要使用Apache文件上傳組件,首先需要安裝Apache文件上傳組件包。在<tomcat的安裝目錄>\webapps\fileupload\WEB-INF目錄中創(chuàng)建一個
名為lib的子目錄,然后從網(wǎng)址http://jakarta.apache.org/commons/fileupload下載到Apache組件的二進制發(fā)行包,在本書的附帶帶光盤中也
提供了該組件的二進制發(fā)行包,文件名為commons-fileupload-1.0.zip。從commons-fileupload-1.0.zip壓縮包中解壓出commons-fileupload
-1.0.jar文件,將它放置進<tomcat的安裝目錄>\webapps\fileupload\WEB-INF\lib目錄中,就完成了Apache文件上傳組件的安裝。
(4)在<tomcat的安裝目錄>\webapps\fileupload目錄中創(chuàng)建一個名為src的子目錄,src目錄用于放置本章編寫的Java源程序。為了便于對
Servlet源文件進行編譯,在src目錄中編寫一個compile.bat批處理文件,如例程1-1所示。
例程1-1 compile.bat
set PATH=C:\jdk1.5.0_01\bin;%path%
set CLASSPATH=C:\tomcat-5.5.12\common\lib\servlet-api.jar;C:\tomcat-5.5.12\\webapps\
fileupload\WEB-INF\lib\commons-fileupload-1.0.jar;%CLASSPATH%
javac -d ..\WEB-INF\classes %1
pause
在compile.bat批處理文件中要注意將commons-fileupload-1.0.jar文件的路徑加入到CLASSPATH環(huán)境變量中和確保編譯后生成的class文件存放
到<tomcat安裝目錄>\webapps\fileupload\WEB-INF\classes目錄中,上面的CLASSPATH環(huán)境變量的設(shè)置值由于排版原因進行了換行,實際上不
應(yīng)該有換行。接著在src目錄中為compile.bat文件創(chuàng)建一個快捷方式,以后只要在Windows資源管理器窗口中將Java源文件拖動到compile.bat
文件的快捷方式上,就可以完成Java源程序的編譯了。之所以要創(chuàng)建compile.bat文件的快捷方式,是因為直接將Java源程序拖動到
compile.bat批處理文件時,compile.bat批處理文件內(nèi)編寫的相對路徑不被支持。創(chuàng)建完的fileupload目錄中的文件結(jié)構(gòu)如圖1.1所示。
圖1.1
(4)啟動Tomcat,在本地計算機的瀏覽器地址欄中輸入如下地址:
http://localhost:8080/fileupload/test.html
驗證瀏覽器能夠成功到該網(wǎng)頁文檔。如果瀏覽器無法訪問到該網(wǎng)頁文檔,請檢查前面的操作步驟和改正問題,直到瀏覽器能夠成功到該網(wǎng)頁文
檔為止。
(5)為了讓/fileupload這個WEB應(yīng)用程序能自動重新裝載發(fā)生了修改的Servlet程序,需要修改Tomcat的server.xml文件,在該文件的<Host>元
素中增加如下一個<Context>子元素:
<Context path="/fileupload" docBase="fileupload" reloadable="true"/>
保存server.xml文件后,重新啟動Tomcat。
1.2 Apache文件上傳組件的應(yīng)用
Java Web開發(fā)人員可以使用Apache文件上傳組件來接收瀏覽器上傳的文件,該組件由多個類共同組成,但是,對于使用該組件來編寫文件上傳
功能的Java Web開發(fā)人員來說,只需要了解和使用其中的三個類:DiskFileUpload、FileItem和FileUploadException。這三個類全部位于
org.apache.commons.fileupload包中。
1.2.1查看API文檔
在準備實驗環(huán)境時獲得的commons-fileupload-1.0.zip文件的解壓縮目錄中可以看到一個docs的子目錄,其中包含了Apache文件上傳組件中的
各個API類的幫助文檔,從這個文檔中可以了解到各個API類的使用幫助信息。打開文件上傳組件API幫助文檔中的index.html頁面,在左側(cè)分欄
窗口頁面中列出了文件上傳組件中的各個API類的名稱,在右側(cè)分欄窗口頁面的底部列出了一段示例代碼,如圖1.2所示。
圖1.2
讀者不需要逐個去閱讀圖1.2中列出的各個API類的幫助文檔,而應(yīng)該以圖1.2中的示例代碼為線索,以其中所使用到的類為入口點,按圖索驥地
進行閱讀,對于示例代碼中調(diào)用到的各個API類的方法則應(yīng)重點掌握。
1.2.2 DiskFileUpload類
DiskFileUpload類是Apache文件上傳組件的核心類,應(yīng)用程序開發(fā)人員通過這個類來與Apache文件上傳組件進行交互。下面介紹
DiskFileUpload類中的幾個常用的重要方法。
1.setSizeMax方法
setSizeMax方法用于設(shè)置請求消息實體內(nèi)容的最大允許大小,以防止客戶端故意通過上傳特大的文件來塞滿服務(wù)器端的存儲空間,單位為字節(jié)
。其完整語法定義如下:
public void setSizeMax(long sizeMax)
如果請求消息中的實體內(nèi)容的大小超過了setSizeMax方法的設(shè)置值,該方法將會拋出FileUploadException異常。
2.setSizeThreshold方法
Apache文件上傳組件在解析和處理上傳數(shù)據(jù)中的每個字段內(nèi)容時,需要臨時保存解析出的數(shù)據(jù)。因為Java虛擬機默認可以使用的內(nèi)存空間是有
限的(筆者測試不大于100M),超出限制時將會發(fā)生“java.lang.OutOfMemoryError”錯誤,如果上傳的文件很大,例如上傳800M的文件,在
內(nèi)存中將無法保存該文件內(nèi)容,Apache文件上傳組件將用臨時文件來保存這些數(shù)據(jù);但如果上傳的文件很小,例如上傳600個字節(jié)的文件,顯然
將其直接保存在內(nèi)存中更加有效。setSizeThreshold方法用于設(shè)置是否使用臨時文件保存解析出的數(shù)據(jù)的那個臨界值,該方法傳入的參數(shù)的單
位是字節(jié)。其完整語法定義如下:
public void setSizeThreshold(int sizeThreshold)
3. setRepositoryPath方法
setRepositoryPath方法用于設(shè)置setSizeThreshold方法中提到的臨時文件的存放目錄,這里要求使用絕對路徑。其完整語法定義如下:
public void setRepositoryPath(String repositoryPath)
如果不設(shè)置存放路徑,那么臨時文件將被儲存在"java.io.tmpdir"這個JVM環(huán)境屬性所指定的目錄中,tomcat 5.5.9將這個屬性設(shè)置為了
“<tomcat安裝目錄>/temp/”目錄。
4. parseRequest方法
parseRequest 方法是DiskFileUpload類的重要方法,它是對HTTP請求消息進行解析的入口方法,如果請求消息中的實體內(nèi)容的類型不是
“multipart/form-data”,該方法將拋出FileUploadException異常。parseRequest 方法解析出FORM表單中的每個字段的數(shù)據(jù),并將它們分別
包裝成獨立的FileItem對象,然后將這些FileItem對象加入進一個List類型的集合對象中返回。parseRequest 方法的完整語法定義如下:
public List parseRequest(HttpServletRequest req)
parseRequest 方法還有一個重載方法,該方法集中處理上述所有方法的功能,其完整語法定義如下:
parseRequest(HttpServletRequest req,int sizeThreshold,long sizeMax,
String path)
這兩個parseRequest方法都會拋出FileUploadException異常。
5. isMultipartContent方法
isMultipartContent方法方法用于判斷請求消息中的內(nèi)容是否是“multipart/form-data”類型,是則返回true,否則返回false。
isMultipartContent方法是一個靜態(tài)方法,不用創(chuàng)建DiskFileUpload類的實例對象即可被調(diào)用,其完整語法定義如下:
public static final boolean isMultipartContent(HttpServletRequest req)
6. setHeaderEncoding方法
由于瀏覽器在提交FORM表單時,會將普通表單中填寫的文本內(nèi)容傳遞給服務(wù)器,對于文件上傳字段,除了傳遞原始的文件內(nèi)容外,還要傳遞其
文件路徑名等信息,如后面的圖1.3所示。不管FORM表單采用的是“application/x-www-form-urlencoded”編碼,還是“multipart/form-data
”編碼,它們僅僅是將各個FORM表單字段元素內(nèi)容組織到一起的一種格式,而這些內(nèi)容又是由某種字符集編碼來表示的。關(guān)于瀏覽器采用何種
字符集來編碼FORM表單字段中的內(nèi)容,請參看筆者編著的《深入體驗java Web開發(fā)內(nèi)幕——核心基礎(chǔ)》一書中的第6.9.2的講解,
“multipart/form-data”類型的表單為表單字段內(nèi)容選擇字符集編碼的原理和方式與“application/x-www-form-urlencoded”類型的表單是
相同的。FORM表單中填寫的文本內(nèi)容和文件上傳字段中的文件路徑名在內(nèi)存中就是它們的某種字符集編碼的字節(jié)數(shù)組形式,Apache文件上傳組
件在讀取這些內(nèi)容時,必須知道它們所采用的字符集編碼,才能將它們轉(zhuǎn)換成正確的字符文本返回。
對于瀏覽器上傳給WEB服務(wù)器的各個表單字段的描述頭內(nèi)容,Apache文件上傳組件都需要將它們轉(zhuǎn)換成字符串形式返回,setHeaderEncoding 方
法用于設(shè)置轉(zhuǎn)換時所使用的字符集編碼,其原理與筆者編著的《深入體驗java Web開發(fā)內(nèi)幕——核心基礎(chǔ)》一書中的第6.9.4節(jié)講解的
ServletRequest.setCharacterEncoding方法相同。setHeaderEncoding 方法的完整語法定義如下:
public void setHeaderEncoding(String encoding)
其中,encoding參數(shù)用于指定將各個表單字段的描述頭內(nèi)容轉(zhuǎn)換成字符串時所使用的字符集編碼。
注意:如果讀者在使用Apache文件上傳組件時遇到了中文字符的亂碼問題,一般都是沒有正確調(diào)用setHeaderEncoding方法的原因。
1.2.3 FileItem類
FileItem類用來封裝單個表單字段元素的數(shù)據(jù),一個表單字段元素對應(yīng)一個FileItem對象,通過調(diào)用FileItem對象的方法可以獲得相關(guān)表單字
段元素的數(shù)據(jù)。FileItem是一個接口,在應(yīng)用程序中使用的實際上是該接口一個實現(xiàn)類,該實現(xiàn)類的名稱并不重要,程序可以采用FileItem接
口類型來對它進行引用和訪問,為了便于講解,這里將FileItem實現(xiàn)類稱之為FileItem類。FileItem類還實現(xiàn)了Serializable接口,以支持序
列化操作。
對于“multipart/form-data”類型的FORM表單,瀏覽器上傳的實體內(nèi)容中的每個表單字段元素的數(shù)據(jù)之間用字段分隔界線進行分割,兩個分隔
界線間的內(nèi)容稱為一個分區(qū),每個分區(qū)中的內(nèi)容可以被看作兩部分,一部分是對表單字段元素進行描述的描述頭,另外一部是表單字段元素的
主體內(nèi)容,如圖1.3所示。
圖 1.3
主體部分有兩種可能性,要么是用戶填寫的表單內(nèi)容,要么是文件內(nèi)容。FileItem類對象實際上就是對圖1.3中的一個分區(qū)的數(shù)據(jù)進行封裝的對
象,它內(nèi)部用了兩個成員變量來分別存儲描述頭和主體內(nèi)容,其中保存主體內(nèi)容的變量是一個輸出流類型的對象。當主體內(nèi)容的大小小于
DiskFileUpload.setSizeThreshold方法設(shè)置的臨界值大小時,這個流對象關(guān)聯(lián)到一片內(nèi)存,主體內(nèi)容將會被保存在內(nèi)存中。當主體內(nèi)容的數(shù)據(jù)
超過DiskFileUpload.setSizeThreshold方法設(shè)置的臨界值大小時,這個流對象關(guān)聯(lián)到硬盤上的一個臨時文件,主體內(nèi)容將被保存到該臨時文件
中。臨時文件的存儲目錄由DiskFileUpload.setRepositoryPath方法設(shè)置,臨時文件名的格式為“upload_00000005(八位或八位以上的數(shù)字)
.tmp”這種形式,F(xiàn)ileItem類內(nèi)部提供了維護臨時文件名中的數(shù)值不重復(fù)的機制,以保證了臨時文件名的唯一性。當應(yīng)用程序?qū)⒅黧w內(nèi)容保存
到一個指定的文件中時,或者在FileItem對象被垃圾回收器回收時,或者Java虛擬機結(jié)束時,Apache文件上傳組件都會嘗試刪除臨時文件,以
盡量保證臨時文件能被及時清除。
下面介紹FileItem類中的幾個常用的方法:
1. isFormField方法
isFormField方法用于判斷FileItem類對象封裝的數(shù)據(jù)是否屬于一個普通表單字段,還是屬于一個文件表單字段,如果是普通表單字段則返回
true,否則返回false。該方法的完整語法定義如下:
public boolean isFormField()
2. getName方法
getName方法用于獲得文件上傳字段中的文件名,對于圖1.3中的第三個分區(qū)所示的描述頭,getName方法返回的結(jié)果為字符串“C:\bg.gif”。
如果FileItem類對象對應(yīng)的是普通表單字段,getName方法將返回null。即使用戶沒有通過網(wǎng)頁表單中的文件字段傳遞任何文件,但只要設(shè)置了
文件表單字段的name屬性,瀏覽器也會將文件字段的信息傳遞給服務(wù)器,只是文件名和文件內(nèi)容部分都為空,但這個表單字段仍然對應(yīng)一個
FileItem對象,此時,getName方法返回結(jié)果為空字符串"",讀者在調(diào)用Apache文件上傳組件時要注意考慮這個情況。getName方法的完整語法
定義如下:
public String getName()
注意:如果用戶使用Windows系統(tǒng)上傳文件,瀏覽器將傳遞該文件的完整路徑,如果用戶使用Linux或者Unix系統(tǒng)上傳文件,瀏覽器將只傳遞該
文件的名稱部分。
3.getFieldName方法
getFieldName方法用于返回表單字段元素的name屬性值,也就是返回圖1.3中的各個描述頭部分中的name屬性值,例如“name=p1”中的“p1”
。getFieldName方法的完整語法定義如下:
public String getFieldName()
4. write方法
write方法用于將FileItem對象中保存的主體內(nèi)容保存到某個指定的文件中。如果FileItem對象中的主體內(nèi)容是保存在某個臨時文件中,該方法
順利完成后,臨時文件有可能會被清除。該方法也可將普通表單字段內(nèi)容寫入到一個文件中,但它主要用途是將上傳的文件內(nèi)容保存在本地文
件系統(tǒng)中。其完整語法定義如下:
public void write(File file)
5.getString方法
getString方法用于將FileItem對象中保存的主體內(nèi)容作為一個字符串返回,它有兩個重載的定義形式:
public java.lang.String getString()
public java.lang.String getString(java.lang.String encoding)
throws java.io.UnsupportedEncodingException
前者使用缺省的字符集編碼將主體內(nèi)容轉(zhuǎn)換成字符串,后者使用參數(shù)指定的字符集編碼將主體內(nèi)容轉(zhuǎn)換成字符串。如果在讀取普通表單字段元
素的內(nèi)容時出現(xiàn)了中文亂碼現(xiàn)象,請調(diào)用第二個getString方法,并為之傳遞正確的字符集編碼名稱。
6. getContentType方法
getContentType 方法用于獲得上傳文件的類型,對于圖1.3中的第三個分區(qū)所示的描述頭,getContentType方法返回的結(jié)果為字符串
“image/gif”,即“Content-Type”字段的值部分。如果FileItem類對象對應(yīng)的是普通表單字段,該方法將返回null。getContentType 方法
的完整語法定義如下:
public String getContentType()
7. isInMemory方法
isInMemory方法用來判斷FileItem類對象封裝的主體內(nèi)容是存儲在內(nèi)存中,還是存儲在臨時文件中,如果存儲在內(nèi)存中則返回true,否則返回
false。其完整語法定義如下:
public boolean isInMemory()
8. delete方法
delete方法用來清空FileItem類對象中存放的主體內(nèi)容,如果主體內(nèi)容被保存在臨時文件中,delete方法將刪除該臨時文件。盡管Apache組件
使用了多種方式來盡量及時清理臨時文件,但系統(tǒng)出現(xiàn)異常時,仍有可能造成有的臨時文件被永久保存在了硬盤中。在有些情況下,可以調(diào)用
這個方法來及時刪除臨時文件。其完整語法定義如下:
public void delete()
1.2.4 FileUploadException類
在文件上傳過程中,可能發(fā)生各種各樣的異常,例如網(wǎng)絡(luò)中斷、數(shù)據(jù)丟失等等。為了對不同異常進行合適的處理,Apache文件上傳組件還開發(fā)
了四個異常類,其中FileUploadException是其他異常類的父類,其他幾個類只是被間接調(diào)用的底層類,對于Apache組件調(diào)用人員來說,只需對
FileUploadException異常類進行捕獲和處理即可。
1.2.5 文件上傳編程實例
下面參考圖1.2中看到的示例代碼編寫一個使用Apache文件上傳組件來上傳文件的例子程序。
:動手體驗:使用Apache文件上傳組件
(1)在<tomcat安裝目錄>\webapps\fileupload目錄中按例程1-1編寫一個名為FileUpload.html的HTML頁面,該頁面用于提供文件上傳的FORM
表單,表單的enctype屬性設(shè)置值為“multipart/form-data”,表單的action屬性設(shè)置為“servlet/UploadServlet”。
例程1-1 FileUpload.html
<html>
<head>
<title>upload experiment</title>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
</head>
<body>
<h3>測試文件上傳組件的頁面</h3>
<form action="servlet/UploadServlet"
enctype="multipart/form-data" method="post">
作者:<input type="text" name="author"><br>
來自:<input type="text" name="company"><br>
文件1:<input type="file" name="file1"><br>
文件2:<input type="file" name="file2"><br>
<input type="submit" value="上載">
</form>
</body>
</html>
(2)在<tomcat的安裝目錄>\webapps\fileupload\src目錄中按例程1-2創(chuàng)建一個名為UploadServlet.java的Servlet程序,UploadServlet.java
調(diào)用Apache文件上傳組件來處理FORM表單提交的文件內(nèi)容和普通字段數(shù)據(jù)。
例程1-2 UploadServlet.java
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.commons.fileupload.*;
import java.util.*;
public class UploadServlet extends HttpServlet
{
public void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException,IOException
{
response.setContentType("text/html;charset=gb2312");
PrintWriter out = response.getWriter();
//設(shè)置保存上傳文件的目錄
String uploadDir = getServletContext().getRealPath("/upload");
if (uploadDir == null)
{
out.println("無法訪問存儲目錄!");
return;
}
File fUploadDir = new File(uploadDir);
if(!fUploadDir.exists())
{
if(!fUploadDir.mkdir())
{
out.println("無法創(chuàng)建存儲目錄!");
return;
}
}
if (!DiskFileUpload.isMultipartContent(request))
{
out.println("只能處理multipart/form-data類型的數(shù)據(jù)!");
return ;
}
DiskFileUpload fu = new DiskFileUpload();
//最多上傳200M數(shù)據(jù)
fu.setSizeMax(1024 * 1024 * 200);
//超過1M的字段數(shù)據(jù)采用臨時文件緩存
fu.setSizeThreshold(1024 * 1024);
//采用默認的臨時文件存儲位置
//fu.setRepositoryPath(...);
//設(shè)置上傳的普通字段的名稱和文件字段的文件名所采用的字符集編碼
fu.setHeaderEncoding("gb2312");
//得到所有表單字段對象的集合
List fileItems = null;
try
{
fileItems = fu.parseRequest(request);
}
catch (FileUploadException e)
{
out.println("解析數(shù)據(jù)時出現(xiàn)如下問題:");
e.printStackTrace(out);
return;
}
//處理每個表單字段
Iterator i = fileItems.iterator();
while (i.hasNext())
{
FileItem fi = (FileItem) i.next();
if (fi.isFormField())
{
String content = fi.getString("GB2312");
String fieldName = fi.getFieldName();
request.setAttribute(fieldName,content);
}
else
{
try
{
String pathSrc = fi.getName();
/*如果用戶沒有在FORM表單的文件字段中選擇任何文件,
那么忽略對該字段項的處理*/
if(pathSrc.trim().equals(""))
{
continue;
}
int start = pathSrc.lastIndexOf('\\');
String fileName = pathSrc.substring(start + 1);
File pathDest = new File(uploadDir, fileName);
fi.write(pathDest);
String fieldName = fi.getFieldName();
request.setAttribute(fieldName, fileName);
}
catch (Exception e)
{
out.println("存儲文件時出現(xiàn)如下問題:");
e.printStackTrace(out);
return;
}
finally //總是立即刪除保存表單字段內(nèi)容的臨時文件
{
fi.delete();
}
}
}
//顯示處理結(jié)果
out.println("用戶:" + request.getAttribute("author") + "<br>");
out.println("來自:" + request.getAttribute("company") + "<br>");
/*將上傳的文件名組合成"file1,file2"這種形式顯示出來,如果沒有上傳
*任何文件,則顯示為"無",如果只上傳了第二個文件,顯示為"file2"。*/
StringBuffer filelist = new StringBuffer();
String file1 = (String)request.getAttribute("file1");
makeUpList(filelist,file1);
String file2 = (String)request.getAttribute("file2");
makeUpList(filelist,file2);
out.println("成功上傳的文件:" +
(filelist.length()==0 ? "無" : filelist.toString()));
}
/**
*將一段字符串追加到一個結(jié)果字符串中。如果結(jié)果字符串的初始內(nèi)容不為空,
*在追加當前這段字符串之前先最加一個逗號(,)。在組合sql語句的查詢條件時,
*經(jīng)常要用到類似的方法,第一條件前沒有"and",而后面的條件前都需要用"and"
*作連詞,如果沒有選擇第一個條件,第二個條件就變成第一個,依此類推。
*
*@param result 要將當前字符串追加進去的結(jié)果字符串
*@param fragment 當前要追加的字符串
*/
private void makeUpList(StringBuffer result,String fragment)
{
if(fragment != null)
{
if(result.length() != 0)
{
result.append(",");
}
result.append(fragment);
}
}
}
在Windows資源管理器窗口中將UploadServlet.java源文件拖動到compile.bat文件的快捷方式上進行編譯,修改Javac編譯程序報告的錯誤,直
到編譯成功通過為止。
(3)修改<tomcat的安裝目錄>\webapps\fileupload\WEB-INF\classes\web.xml文件,在其中注冊和映射UploadServlet的訪問路徑,如例程1-3
所示。
例程1-3 web.xml
<web-app>
<servlet>
<servlet-name>UploadServlet</servlet-name>
<servlet-class>UploadServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UploadServlet</servlet-name>
<url-pattern>/servlet/UploadServlet</url-pattern>
</servlet-mapping>
</web-app>
(4)重新啟動Tomcat,并在瀏覽器地址欄中輸入如下地址:
http://localhost:8080/fileupload/FileUpload.html
填寫返回頁面中的FORM表單,如圖1.4所示,單擊“上載”按鈕后,瀏覽器返回的頁面信息如圖1.5所示。
圖1.4
圖1.5(這些圖的標題欄中的it315改為fileupload)
查看<tomcat安裝目錄>\webapps\it315\upload目錄,可以看到剛才上傳的兩個文件。
(4)單擊瀏覽器工具欄上的“后退”按鈕回到表單填寫頁面,只在第二個文件字段中選擇一個文件,單擊“上載”按鈕,瀏覽器返回的顯示結(jié)果
如圖1.6所示。
圖1.6
M腳下留心:
上面編寫的Servlet程序?qū)⑸蟼鞯奈募4嬖诹水斍癢EB應(yīng)用程序下面的upload目錄中,這個目錄是客戶端瀏覽器可以訪問到的目錄。如果用戶
通過瀏覽器上傳了一個名稱為test.jsp的文件,那么用戶接著就可以在瀏覽器中訪問這個test.jsp文件了,對于本地瀏覽器來說,這個jsp文件
的訪問URL地址如下所示:
http://localhost:8080/fileupload/upload/test.jsp
對于遠程客戶端瀏覽器而言,只需要將上面的url地址中的localhost改寫為Tomcat服務(wù)器的主機名或IP地址即可。用戶可以通過上面的Servlet
程序來上傳自己編寫的jsp文件,然后又可以通過瀏覽器來訪問這個jsp文件,如果用戶在jsp文件中編寫一些有害的程序代碼,例如,查看服務(wù)
器上的所有目錄結(jié)構(gòu),調(diào)用服務(wù)器上的操作系統(tǒng)進程等等,這將是一個非常致命的安全漏洞和隱患,這臺服務(wù)器對外就沒有任何安全性可言了
。
1.3 Apache文件上傳組件的源碼賞析
經(jīng)常閱讀一些知名的開源項目的源代碼,可以幫助我們開闊眼界和快速提高編程能力。Apache文件上傳組件是Apache組織開發(fā)的一個開源項目
,從網(wǎng)址http://jakarta.apache.org/commons/fileupload可以下載到Apache組件的源程序包,在本書的附帶帶光盤中也提供了該組件的源程
序包,文件名為commons-fileupload-1.0-src.zip。該組件的設(shè)計思想和程序編碼細節(jié)包含有許多值得借鑒的技巧,為了便于有興趣的讀者學(xué)
習(xí)和研究該組件的源碼,本節(jié)將分析Apache文件上傳組件的源代碼實現(xiàn)。對于只想了解如何使用Apache文件上傳組件來上傳文件的讀者來說,
不必學(xué)習(xí)本節(jié)的內(nèi)容。在學(xué)習(xí)本節(jié)內(nèi)容之前,讀者需要仔細學(xué)習(xí)了筆者編著的《深入體驗java Web開發(fā)內(nèi)幕——核心基礎(chǔ)》一書中的第6.7.2節(jié)
中講解的“分析文件上傳的請求消息結(jié)構(gòu)”的知識。
1.3.1 Apache文件上傳組的類工作關(guān)系
Apache文件上傳組件總共由兩個接口,十二個類組成。在Apache文件上傳組件的十二個類中,有兩個抽象類,四個的異常類,六個主要類,其
中FileUpLoad類用暫時沒有應(yīng)用,是為了以后擴展而保留的。Apache文件上傳組件中的各個類的關(guān)系如圖1.7所示,圖中省略了異常類。
圖 1.7
DiskFileUpload類是文件上傳組件的核心類,它是一個總的控制類,首先由Apache文件上傳組件的使用者直接調(diào)用DiskFileUpload類的方法,
DiskFileUpload類再調(diào)用和協(xié)調(diào)更底層的類來完成具體的功能。解析類MultipartStream和工廠類DefaultFileItemFactory就是DiskFileUpload
類調(diào)用的兩個的底層類。MultipartStream類用于對請求消息中的實體數(shù)據(jù)進行具體解析,DefaultFileItemFactory類對MultipartStream類解
析出來的數(shù)據(jù)進行封裝,它將每個表單字段數(shù)據(jù)封裝成一個個的FileItem類對象,用戶通過FileItem類對象來獲得相關(guān)表單字段的數(shù)據(jù)。
DefaultFileItem是FileItem接口的實現(xiàn)類,實現(xiàn)了FileItem接口中定義的功能,用戶只需關(guān)心FileItem接口,通過FileItem接口來使用
DefaultFileItem類實現(xiàn)的功能。DefaultFileItem類使用了兩個成員變量來分別存儲表單字段數(shù)據(jù)的描述頭和主體內(nèi)容,其中保存主體內(nèi)容的
變量類型為DeferredFileOutputStream類。DeferredFileOutputStream類是一個輸出流類型,在開始時,DeferredFileOutputStream類內(nèi)部使
用一個ByteArrayOutputStream類對象來存儲數(shù)據(jù),當寫入它里面的主體內(nèi)容的大小大于DiskFileUpload.setSizeThreshold方法設(shè)置的臨界值
時,DeferredFileOutputStream類內(nèi)部創(chuàng)建一個文件輸出流對象來存儲數(shù)據(jù),并將前面寫入到ByteArrayOutputStream類對象中的數(shù)據(jù)轉(zhuǎn)移到文
件輸出流對象中。這個文件輸出流對象關(guān)聯(lián)的文件是一個臨時文件,它的保存路徑由DiskFileUpload.setRepositoryPath方法指定。
Apache文件上傳組件的處理流程如圖1.8所示。
圖1.8
圖1.8中的每一步驟的詳細解釋如下:
(1)Web容器接收用戶的HTTP請求消息,創(chuàng)建request請求對象。
(2)調(diào)用DiskFileUpload類對象的parseRequest方法對request請求對象進行解析。該方法首先檢查request請求對象中的數(shù)據(jù)內(nèi)容是否是
“multipart/form-data”類型,如果是,該方法則創(chuàng)建MultipartStream類對象對request請求對象中的請求體 進行解析。
(3)MultipartStream類對象對request請求體進行解析,并返回解析出的各個表單字段元素對應(yīng)的內(nèi)容。
(4)DiskFileUpload類對象的parseRequest方法接著創(chuàng)建DefaultFileItemFactory類對象,用來將MultipartStream類對象解析出的每個表單
字段元素的數(shù)據(jù)封裝成FileItem類對象。
(5)DefaultFileItemFactory工廠類對象把MultipartStream類對象解析出的各個表單字段元素的數(shù)據(jù)封裝成若干DefaultFileItem類對象,然
后加入到一個List類型的集合對象中,parseRequest方法返回該List集合對象。
實際上,步驟(3)和步驟(5)是交替同步進行的,即在MultipartStream類對象解析每個表單字段元素時,都會調(diào)用DefaultFileItemFactory
工廠類把該表單字段元素封裝成對應(yīng)的FileItem類對象。
1.3.2 Apache文件上傳組件的核心編程問題
WEB服務(wù)器端程序接收到“multipart/form-data”類型的HTTP請求消息后,其核心和基本的編程工作就是讀取請求消息中的實體內(nèi)容,然后解
析出每個分區(qū)的數(shù)據(jù),接著再從每個分區(qū)中解析出描述頭和主體內(nèi)容部分。
在讀取HTTP請求消息中的實體內(nèi)容時,只能調(diào)用HttpServletRequest.getInputStream方法返回的字節(jié)輸入流,而不能調(diào)用
HttpServletRequest.getReader方法返回的字符輸入流,因為不管上傳的文件類型是文本的、還是其他各種格式的二進制內(nèi)容,WEB服務(wù)器程序
要做的工作就是將屬于文件內(nèi)容的那部分數(shù)據(jù)原封不動地提取出來,然后原封不動地存儲到本地文件系統(tǒng)中。如果使用
HttpServletRequest.getReader方法返回的字符輸入流對象來讀取HTTP請求消息中的實體內(nèi)容,它將HTTP請求消息中的字節(jié)數(shù)據(jù)轉(zhuǎn)換成字符后
再返回,這主要是為了方便要以文本方式來處理本來就全是文本內(nèi)容的請求消息的應(yīng)用,但本程序要求的是“原封不動”,顯然不能使用
HttpServletRequest.getReader方法返回的字符輸入流對象來進行讀取。
另外,不能期望用一個很大的字節(jié)數(shù)組就可以裝進HTTP請求消息中的所有實體內(nèi)容,因為程序中定義的字節(jié)數(shù)組大小總是有限制的,但應(yīng)該允
許客戶端上傳超過這個字節(jié)數(shù)組大小的實體內(nèi)容。所以,只能創(chuàng)建一個一般大小的字節(jié)數(shù)組緩沖區(qū)來逐段讀取請求消息中的實體內(nèi)容,讀取一
段就處理一段,處理完上一段以后,再讀取下一段,如此循環(huán),直到處理完所有的實體內(nèi)容,如圖1.9所示。
圖 1.9
在圖1.9中,buffer即為用來逐段讀取請求消息中的實體內(nèi)容的字節(jié)數(shù)組緩沖區(qū)。因為讀取到緩沖區(qū)中的數(shù)據(jù)處理完后就會被拋棄,確切地說,
是被下一段數(shù)據(jù)覆蓋,所以,解析和封裝過程必須同步進行,程序一旦識別出圖1.3中的一個分區(qū)的開始后,就要開始將它封裝到一個FileItem
對象中。
程序要識別出圖1.3中的每一個分區(qū),需要在圖1.9所示的字節(jié)數(shù)組緩沖區(qū)buffer中尋找分區(qū)的字段分隔界線,當找到一個字段分隔界線后,就
等于找到了一個分區(qū)的開始。筆者在《深入體驗java Web開發(fā)內(nèi)幕——核心基礎(chǔ)》一書中的第6.7.2節(jié)中已經(jīng)講過,上傳文件的請求消息的
Content-Type頭字段中包含有用作字段分隔界線的字符序列,如下所示:
content-type : multipart/form-data; boundary=---------------------------7d51383203e8
顯然,我們可以通過調(diào)用HttpServletRequest.getHeader方法讀取Content-Type頭字段的內(nèi)容,從中分離出分隔界線的字符序列,然后在字節(jié)
數(shù)組緩沖區(qū)buffer中尋找分區(qū)的字段分隔界線。content-type頭字段的boundary參數(shù)中指定的字段分隔界線是瀏覽器隨機產(chǎn)生的,瀏覽器保證
它不會與用戶上傳的所有數(shù)據(jù)中的任何部分出現(xiàn)相同。在這里有一點需要注意,圖1.3中的實體內(nèi)容內(nèi)部的字段分隔界線與content-type頭中指
定的字段分隔界線有一點細微的差別,前者是在后者前面增加了兩個減號(-)字符而形成的,這倒不是什么編程難點。真正的編程難點在于在
字節(jié)數(shù)組緩沖區(qū)buffer中尋找分隔界線時,可能會遇到字節(jié)數(shù)組緩沖區(qū)buffer中只裝入了分隔界線字符序列的部分內(nèi)容的情況,如圖1.10所示
。
圖1.10
要解決這個問題的方法之一就是在查找字段分隔界線時,如果發(fā)現(xiàn)字節(jié)數(shù)組緩沖區(qū)buffer中只裝入了分隔界線字符序列的部分內(nèi)容,那么就將
這一部分內(nèi)容留給字節(jié)數(shù)組緩沖區(qū)buffer的下一次讀取,如圖1.11所示。
圖1.11
這種方式讓字節(jié)數(shù)組緩沖區(qū)buffer下一次讀取的內(nèi)容不是緊接著上一次讀取內(nèi)容的后面,而是重疊上一次讀取的一部分內(nèi)容,即從上一次讀取
內(nèi)容中的分隔界線字符序列的第一個字節(jié)處開始讀取。這種方式在實際的編程處理上存在著相當大的難度,程序首先必須確定字節(jié)數(shù)組緩沖區(qū)
buffer上一次讀取的數(shù)據(jù)的后一部分內(nèi)容正好是分隔界線字符序列的前面一部分內(nèi)容,而這一部分內(nèi)容的長度是不確定的,可能只是分隔界線
字符序列的第一個字符,也可能是分隔界線字符序列的前面n-1個字符,其中n為分隔界線字符序列的整個長度。另外,即使確定字節(jié)數(shù)組緩沖
區(qū)buffer上一次讀取的數(shù)據(jù)的后一部分內(nèi)容正好是分隔界線字符序列的前面一部分內(nèi)容,但它們在整個輸入字節(jié)流中的后續(xù)內(nèi)容不一定就整個
分隔界線字符序列的后一部分內(nèi)容,出現(xiàn)這種情況的可能性是完全存在,程序必須進行全面和嚴謹?shù)目紤]。
Apache文件上傳組件的解決方法比較巧妙,它在查找字段分隔界線時,如果搜索到最后第n個字符時,n為分隔界線字符序列的長度,發(fā)現(xiàn)最后n
個字符不能與分隔界線字符序列匹配,則將最后的n-1個字符留給字節(jié)數(shù)組緩沖區(qū)buffer的下一次讀取,程序再對buffer的下一次讀取的整個內(nèi)
容從頭開始查找字段分隔界線,如圖1.12所示。
圖1.12
Apache文件上傳組件查找字段分隔界線的具體方法,讀者可以請參見MultipartStream類的findSeparator()方法中的源代碼。
當找到一個分區(qū)的開始位置后,程序還需要分辨出分區(qū)中的描述頭和主體內(nèi)容,并對這兩部分內(nèi)容分開存儲。如何分辨出一個分區(qū)的描述頭和
主體部分呢?從圖1.3中可以看到,每個分區(qū)中的描述頭和主體內(nèi)容之間有一空行,再加上描述頭后面的換行,這就說明描述頭和主體部分之間
是使用“\n”、“\r”、“\n”、“\r”這四個連續(xù)的字節(jié)內(nèi)容進行分隔。因此,程序需要把“\n”、“\r”、“\n”、“\r”這四個連續(xù)的
字節(jié)內(nèi)容作為描述頭和主體部分之間的分隔界線,并在字節(jié)數(shù)組緩沖區(qū)buffer中尋找這個特殊的分隔界線來識別描述頭和主體部分。
當識別出一個分區(qū)中的描述頭和主體部分后,程序需要解決的下一個問題就是如何將描述頭和主體部分的數(shù)據(jù)保存到FileItem對象中,以便用
戶以后可以調(diào)用FileItem類的方法來獲得這些數(shù)據(jù)。主體部分的數(shù)據(jù)需要能夠根據(jù)用戶上傳的文件大小有伸縮性地進行存儲,因此,程序要求
編寫一個特殊的類來封裝主體部分的數(shù)據(jù),對于這個問題的具體實現(xiàn)細節(jié),讀者可參見1.2.4小節(jié)中講解的DeferredFileOutputStream類來了解
1.3.3 MultipartStream類
MultipartStream類用來對上傳的請求輸入流進行解析,它是整個Apache上傳組件中最復(fù)雜的類。
1.設(shè)計思想
MultipartStream類中定義了一個byte[]類型的boundary成員變量,這個成員變量用于保存圖1.3中的各個數(shù)據(jù)分區(qū)之間的分隔界線,每個分區(qū)
分別代表一個表單字段的信息。圖1.3中的每個分區(qū)又可以分為描述頭部分和主體部分,MultipartStream類中定義了一個readHeaders()方法來
讀取描述頭部分的內(nèi)容,MultipartStream類中定義了一個readBodyData(OutputStream output)方法來讀取主體部分的內(nèi)容,并將這些內(nèi)容寫
入到一個作為參數(shù)傳入進來的輸出流對象中。readBodyData方法接收的參數(shù)output對象在應(yīng)用中的實際類型是DeferredFileOutputStream,這
個對象又是保存在DefaultFileItem類對象中的一個成員變量,這樣,readBodyData方法就可以將一個分區(qū)的主體部分的數(shù)據(jù)寫入到
DefaultFileItem類對象中。
因為圖1.3中的實體內(nèi)容內(nèi)部的字段分隔界線是在content-type頭中指定的字段分隔界線前面增加了兩個減號(-)字符而形成的,而每個字段
分隔界線與它前面內(nèi)容之間還進行了換行,這個換行并不屬于表單字段元素的內(nèi)容。所以,MultipartStream類中的成員變量boundary中存儲的
字節(jié)數(shù)組并不是直接從content-type頭的boundary參數(shù)中獲得的字符序列,而是在boundary參數(shù)中指定的字符序列前面增加了四個字節(jié),依次
是‘\n’、‘\r’、‘-’和‘-’。MultipartStream類中定義了一個readBoundary()方法來讀取和識別各個字段之間分隔界線,有一點特殊的
是,圖1.3中的第一個分隔界線前面沒有回車換行符,它是無法與成員變量boundary中的數(shù)據(jù)相匹配的,所以無法調(diào)用readBoundary()方法進行
讀取,而是需要進行特殊處理,其后的每個分隔界線都與boundary中的數(shù)據(jù)相匹配,可以直接調(diào)用readBoundary()方法進行讀取。在本章的后
面部分,如果沒有特別說明,所說的分隔界線都是指成員變量boundary中的數(shù)據(jù)內(nèi)容。
RFC 1867格式規(guī)范規(guī)定了描述頭和主體部分必須用一個空行進行分隔,如圖1.3所示,也就是描述頭和主體部分使用“\n”、“\r”、“\n”、
“\r”這四個連續(xù)的字節(jié)內(nèi)容進行分隔。MultipartStream類的設(shè)計者為了簡化編程,在readHeaders()方法中將“\n”、“\r”、“\n”、
“\r”這四個連續(xù)的字節(jié)內(nèi)容連同描述頭一起進行讀取。readHeaders()方法在讀取數(shù)據(jù)的過程中,當它發(fā)現(xiàn)第一個‘\n’、‘\r’、‘\n’、
‘\r’ 連續(xù)的字節(jié)序列時就會返回,即使主體部分正好也包含了“\n”、“\r”、“\n”、“\r”這四個連續(xù)的字節(jié)內(nèi)容,但是,它們只會被
隨后調(diào)用的readBodyData方法作為主體內(nèi)容讀取,永遠不會被readHeaders()方法讀取到,所以,它們不會與作為描述頭和主體部分的分隔字符
序列發(fā)生沖突。
由于readHeaders()方法讀取了一個分區(qū)中的主體部分前面的所有內(nèi)容(包括它前面的換行),而它與下一個分區(qū)之間的分隔界線前面的換行又
包含在了成員變量boundary中,這個換行將被readBoundary()方法讀取,所以,夾在readheaders()方法讀取的內(nèi)容和readBoundary()方法讀取
的內(nèi)容之間的數(shù)據(jù)全部都屬于表單字段元素的內(nèi)容了,因此,讀取分區(qū)中的主體部分的readBodyData(OutputStream output)方法不需要進行特
別的處理,它直接將讀取的數(shù)據(jù)寫入到DefaultFileItem類對象中封裝的DeferredFileOutputStream屬性對象中即可。
2. 構(gòu)造方法
MultipartStream類中的一個主要的構(gòu)造方法的語法定義如下:
public (InputStream input, byte[] boundary, int bufSize)
其中,參數(shù)input是指從HttpServetRequest請求對象中獲得的字節(jié)輸入流對象,參數(shù)boundary是從請求消息頭中獲得的未經(jīng)處理的分隔界線,
bufSize指定圖1.10中的buffer緩沖區(qū)字節(jié)數(shù)組的長度,默認值是4096個字節(jié)。這個構(gòu)造方法的源代碼如下:
public MultipartStream(InputStream input, byte[] boundary, int bufSize)
{
// 初始化成員變量
this.input = input;
this.bufSize = bufSize;
this.buffer = new byte[bufSize];
this.boundary = new byte[boundary.length + 4];
this.boundaryLength = boundary.length + 4;
//buffer緩沖區(qū)中保留給下次讀取的最大字節(jié)個數(shù)
this.keepRegion = boundary.length + 3;
this.boundary[0] = 0x0D; //‘\n’的16進制形式
this.boundary[1] = 0x0A; //‘\r’的16進制形式
this.boundary[2] = 0x2D; //‘-’的16進制形式
this.boundary[3] = 0x2D;
//在成員變量boundary中生成最終的分隔界線
System.arraycopy (boundary, 0, this.boundary, 4, boundary.length);
head = 0; // 成員變量,表示正在處理的這個字節(jié)在buffer中的位置指針
tail = 0; // 成員變量,表示實際讀入到buffer中的字節(jié)個數(shù)
}
3. readByte方法
MultipartStream類中的readByte()方法從字節(jié)數(shù)組緩沖區(qū)buffer中讀一個字節(jié),當buffer緩沖區(qū)中沒有更多的數(shù)據(jù)可讀時,該方法會自動從輸
入流中讀取一批新的字節(jié)數(shù)據(jù)來重新填充buffer緩沖區(qū)。readByte()方法的源代碼如下:
public byte readByte () throws IOException
{
// 判斷是否已經(jīng)讀完了buffer緩沖區(qū)中的所有數(shù)據(jù)
if (head == tail)
{
head = 0;
//讀入新的數(shù)據(jù)內(nèi)容來填充buffer緩沖區(qū)
tail = input.read(buffer, head, bufSize);
if (tail == -1)
{
throw new IOException("No more data is available ");
}
}
return buffer[head++];// 返回當前字節(jié),head++
}
其中,head變量是MultipartStream類中定義的一個int類型的成員變量,它用于表示正在讀取的字節(jié)在buffer數(shù)組緩沖區(qū)中的位置;tail變量
也是MultipartStream類中定義的一個int類型的成員變量,它用于表示當前buffer數(shù)組緩沖區(qū)裝入的實際字節(jié)內(nèi)容的長度。在MultipartStream
類中主要是通過控制成員變量head的值來控制對buffer緩沖區(qū)中的數(shù)據(jù)的讀取和直接跳過某段數(shù)據(jù),通過比較head與tail變量的值了解是否需
要向buffer緩沖區(qū)中裝入新的數(shù)據(jù)內(nèi)容。當每次向buffer緩沖區(qū)中裝入新的數(shù)據(jù)內(nèi)容后,都應(yīng)該調(diào)整成員變量head和tail的值。
4. arrayequals靜態(tài)方法
MultipartStream類中定義了一個的arrayequals靜態(tài)方法,用于比較兩個字節(jié)數(shù)組中的前面一部分內(nèi)容是否相等,相等返回true,否則返回
false。arrayequals方法的源代碼如下,參數(shù)count指定了對字節(jié)數(shù)組中的前面幾個字節(jié)內(nèi)容進行比較:
public static boolean arrayequals(byte[] a, byte[] b,int count)
{
for (int i = 0; i < count; i++)
{
if (a[i] != b[i])
{
return false;
}
}
return true;
}
5. findByte方法
MultipartStream類中的findByte()方法從字節(jié)數(shù)組緩沖區(qū)buffer中的某個位置開始搜索一個特定的字節(jié)數(shù)據(jù),如果找到了,則返回該字節(jié)在
buffer緩沖區(qū)中的位置,不再繼續(xù)搜索,如果沒有找到,則返回-1。findByte方法的源代碼如下,參數(shù)pos制定了不搜索的起始位置值,value
是要搜索的字節(jié)數(shù)據(jù):
protected int findByte(byte value,int pos)
{
for (int i = pos; i < tail; i++)
{
if (buffer[i] == value)
{
return i; // 找到該值,findByte方法返回
}
}
return - 1;
}
如果程序需要在buffer緩沖區(qū)中多次搜索某個特定的字節(jié)數(shù)據(jù),那就可以循環(huán)調(diào)用findByte方法,只是在每次調(diào)用findByte方法時,必須不斷
地改變參數(shù)pos的值,讓pos的值等于上次調(diào)用findByte的返回值,直到findByte方法返回-1時為止,如圖1.13所示。
圖1.13
6. findSeparator方法
MultipartStream類中的findSeparator方法用于從字節(jié)數(shù)組緩沖區(qū)buffer中查找成員變量boundary中定義的分隔界線,并返回分隔界線的第一
個字節(jié)在buffer緩沖區(qū)中的位置,如果在buffer緩沖區(qū)中沒有找到分隔界線,則返回-1。
findSeparator方法內(nèi)部首先調(diào)用findByte方法在buffer緩沖區(qū)中搜索分隔界線boundary的第一個字節(jié)內(nèi)容,如果沒有找到,則說明buffer緩沖
區(qū)中沒有包含分隔界線;如果findByte方法在buffer緩沖區(qū)中找到了分隔界線boundary的第一個字節(jié)內(nèi)容,findSeparator方法內(nèi)部接著確定該
字節(jié)及隨后的字節(jié)序列是否確實是分隔界線。findSeparator方法內(nèi)部循環(huán)調(diào)用findByte方法,直到找到分隔界線或者findByte方法已經(jīng)查找到
了buffer緩沖區(qū)中的最后boundaryLength -1個字節(jié)。findSeparator方法內(nèi)部為什么調(diào)用findByte方法查找到buffer緩沖區(qū)中的最后
boundaryLength-1個字節(jié)時就停止查找呢?這是為了解決如圖1.10所示的buffer緩沖區(qū)中裝入了分隔界線的部分內(nèi)容的特殊情況,所以在
findSeparator()方法中不要搜索buffer緩沖區(qū)中的最后的boundaryLength -1個字節(jié),而是把buffer緩沖區(qū)中的最后這boundaryLength -1個字
節(jié)作為保留區(qū),在下次讀取buffer緩沖區(qū)時將這些保留的字節(jié)數(shù)據(jù)重新填充到buffer緩沖區(qū)的開始部分。findSeparator方法的源代碼如下:
protected int findSeparator()
{
int first;
int match = 0;
int maxpos = tail - boundaryLength;//在buffer中搜索的最大位置
for (first = head;(first <= maxpos) && (match != boundaryLength);
first++)
{
//在buffer緩沖區(qū)中尋找boundary的第一個字節(jié)
first = findByte(boundary[0], first);
/*buffer中找不到boundary[0]或者boundary[0]位于保留區(qū)中,
則可以判斷buffer中不存在分隔界線*/
if (first == -1 || (first > maxpos))
{
return -1;
}
//確定隨后的字節(jié)序列是否確實是分隔界線的其他字節(jié)內(nèi)容
for (match = 1; match < boundaryLength; match++)
{
if (buffer[first + match] != boundary[match])
{
break;
}
}
}
// 當前buffer中找到boundary,返回第一個字節(jié)所在位置值
if (match == boundaryLength)
{
return first - 1;
}
return -1; // 當前buffer中沒找到boundary,返回-1
}
圖1.14中描述了findSeparator方法內(nèi)部定義的各個變量的示意圖。
圖1.14
findSeparator方法內(nèi)部的代碼主要包括如下三個步驟:
(1)循環(huán)調(diào)用findByte(boundary[0], first)找到buffer緩沖區(qū)中的與boundary[0]相同的字節(jié)的位置,并將位置記錄在first變量中。
(2)比較buffer緩沖區(qū)中的first后的boundaryLength-1個字節(jié)序列是否與boundary中的其他字節(jié)序列相同。如果不同,說明這個first變量指
向的字節(jié)不是分隔界線的開始字節(jié),跳出內(nèi)循環(huán),將first變量加1后繼續(xù)外循環(huán)調(diào)用findByte方法;如果相同,說明在當前緩沖區(qū)buffer中找
到了分隔界線,內(nèi)循環(huán)正常結(jié)束,此時match變量的值為boundaryLength,接著執(zhí)行外循環(huán)將first變量加1,然后執(zhí)行外循環(huán)的條件判斷,由于
match != boundaryLength條件不成立,外循環(huán)也隨之結(jié)束。
(3)判斷match是否等于boundaryLength,如果等于則說明找到了分隔界線,此時返回成員變量boundary的第一個字節(jié)在緩沖區(qū)buffer中位置
,由于第(2)中將first加1了,所以這里的返回值應(yīng)該是first-1;如果不等,說明當前緩沖區(qū)huffer中沒有分隔界線,返回-1。
7. readHeaders方法
MultipartStream類中的readHeaders方法用于讀取一個分區(qū)的描述頭部分,并根據(jù)DiskFileUpload類的setHeaderEncoding方法設(shè)定的字符集編
碼將描述頭部分轉(zhuǎn)換成一個字符串返回。
在調(diào)用readHeaders方法之前時,程序已經(jīng)調(diào)用了findSeparator方法找到了分隔界線和讀取了分隔界線前面的內(nèi)容,此時MultipartStream類中
的成員變量head指向了buffer緩沖區(qū)中的分隔界線boundary的第一個字節(jié),程序接著應(yīng)調(diào)用readBoundary方法跳過分隔界線及其隨后的回車換
行兩個字節(jié),以保證在調(diào)用readHeaders方法時,成員變量head已經(jīng)指向了分區(qū)的描述頭的第一個字節(jié)。在readHeaders方法內(nèi)部,直接循環(huán)調(diào)
用readByte方法讀取字節(jié)數(shù)據(jù),并把讀到的數(shù)據(jù)存儲在一個字節(jié)數(shù)組輸出流中,直到讀取到了連續(xù)的兩次回車換行字符,就認為已經(jīng)讀取完了
描述頭的全部內(nèi)容,此時成員變量head將指向分區(qū)中的主體內(nèi)容的第一個字節(jié)。readHeaders()方法的源代碼如下:
public String readHeaders()throws MalformedStreamException
{
int i = 0;
//從下面的代碼看來,這里定義成一個byte即可,不用定義成byte數(shù)組
byte b[] = new byte[1];
//用于臨時保存描述頭信息的字節(jié)數(shù)組輸出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//對描述頭部分的數(shù)據(jù)內(nèi)容過大進行限制處理
int sizeMax = HEADER_PART_SIZE_MAX;
int size = 0;
while (i < 4)
{
try
{
b[0] = readByte(); }
catch (IOException e)
{
throw new MalformedStreamException("Stream ended unexpectedly " );
}
size++;
//靜態(tài)常量HEADER_SEPARATOR的值為:{0x0D, 0x0A, 0x0D, 0x0A}
if (b[0] == HEADER_SEPARATOR[i])
{
i++;
}
else
{
i = 0;
}
if (size <= sizeMax)
{
baos.write(b[0]); // 將當前字節(jié)存入緩沖流
}
}
String headers = null; // 找到HEADER_SEPARATOR后,獲取描述頭
if (headerEncoding != null)
{
try
{
headers = baos.toString(headerEncoding);
}
catch (UnsupportedEncodingException e)
{
headers = baos.toString();
}
}
else
{
headers = baos.toString();
}
return headers;
}
readHeaders方法循環(huán)調(diào)用readByte()方法逐個讀取buffer緩沖區(qū)中的字節(jié),并將讀取的字節(jié)與HEADER_SEPARATOR ={‘\n’,‘\r’,‘\n’
,‘\r’}的第一個字節(jié)進行比較,如果這個字節(jié)等于HEADER_SEPARATOR的首字節(jié)‘\n’,則循環(huán)控制因子i加1,這樣,下次調(diào)用readByte()方
法讀取的字節(jié)將與HEADER_SEPARATOR中的第二字節(jié)比較,如果相等,則依照這種方式比較后面的字節(jié)內(nèi)容,如果連續(xù)讀取到了
HEADER_SEPARATOR字節(jié)序列,則循環(huán)語句結(jié)束。readHeaders方法將讀取到的每個正常字節(jié)寫入到了一個字節(jié)數(shù)組輸出流中,其中也包括作為描
述頭與主體內(nèi)容之間的分隔序列HEADER_SEPARATOR中的字節(jié)數(shù)據(jù)。由于readByte()方法會自動移動head變量的值和自動向緩沖區(qū)buffer中載入
數(shù)據(jù),所以,readHeaders方法執(zhí)行完以后,成員變量head指向分區(qū)主體部分的首字節(jié)。readHeaders方法最后將把存入字節(jié)數(shù)組輸出流中的字
節(jié)數(shù)據(jù)按指定字符集編碼轉(zhuǎn)換成字符串并返回,該字符串就是描述頭字符串。
8. readBodyData方法
MultipartStream類中的readBodyData方法用于把主體部分的數(shù)據(jù)寫入到一個輸出流對象中,并返回寫入到輸出流中的字節(jié)總數(shù)。當調(diào)用
readBodyData方法前,成員變量head已經(jīng)指向了分區(qū)的主體部分的首字節(jié),readBodyData方法調(diào)用完成后,成員變量head指向分區(qū)分隔界線的
首字節(jié)。readBodyData方法中需要調(diào)用findSeparator方法找出下一個分區(qū)分隔界線的首字節(jié)位置,才能知道這次讀取的分區(qū)主體內(nèi)容的結(jié)束位
置。從分區(qū)主體部分的首字節(jié)開始,直到在findSeparator方法找到的下一個分區(qū)分隔界線前的所有數(shù)據(jù)都是這個分區(qū)的主體部分的數(shù)據(jù),
readBodyData方法需要把這些數(shù)據(jù)都寫到輸出流output對象中。如果findSeparator方法在buffer緩沖區(qū)中沒有找到分區(qū)分隔界線,
readBodyData方法還必須向buffer緩沖區(qū)中裝入新的數(shù)據(jù)內(nèi)容后繼續(xù)調(diào)用findSeparator方法進行處理。在向buffer緩沖區(qū)中裝入新的數(shù)據(jù)內(nèi)容
時,必須先將上次保留在buffer緩沖區(qū)中的內(nèi)容轉(zhuǎn)移進新buffer緩沖區(qū)的開始處。readBodyData方法的源代碼如下,傳遞給readBodyData方法
的參數(shù)實際上是一個DeferredFileOutputStream類對象:
public int readBodyData(OutputStream output)
throws MalformedStreamException,IOException
{
// 用于控制循環(huán)的變量
boolean done = false;
int pad;
int pos;
int bytesRead;
// 寫入到輸出流中的字節(jié)個數(shù)
int total = 0;
while (!done)
{
pos = findSeparator();// 搜索分隔界線
if (pos != -1) //緩沖區(qū)buffer中包含有分隔界線
{
output.write(buffer, head, pos - head);
total += pos - head;
head = pos;//head變量跳過主體數(shù)據(jù),指向分隔界線的首字節(jié)
done = true;// 跳出循環(huán)
}
else //緩沖區(qū)buffer中沒有包含分隔界線
{
/*根據(jù)緩沖區(qū)中未被readHeaders方法讀取的數(shù)據(jù)內(nèi)容是否大于圖1.4中的
保留區(qū)的大小,來決定保留到下一次buffer緩沖區(qū)中的字節(jié)個數(shù)
*/
if (tail - head > keepRegion)
{
pad = keepRegion;
}
else
{
pad = tail - head;
}
output.write(buffer, head, tail - head - pad);
total += tail - head - pad;//統(tǒng)計寫入到輸出流中的字節(jié)個數(shù)
/*將上一次buffer緩沖區(qū)中的未處理的數(shù)據(jù)轉(zhuǎn)移到
下一次buffer緩沖區(qū)的開始位置
*/
System.arraycopy(buffer, tail - pad, buffer, 0, pad);
head = 0; //讓head變量指向緩沖區(qū)的開始位置
//向buffer緩沖區(qū)中載入新的數(shù)據(jù)
bytesRead = input.read(buffer, pad, bufSize - pad);
if (bytesRead != -1)
{
//設(shè)置buffer緩沖區(qū)中的有效字節(jié)的個數(shù)
tail = pad + bytesRead;
}
else
{
/*還沒有找到分隔界線,輸入流就結(jié)束了,輸入流中的數(shù)據(jù)格式
顯然不正確,保存緩沖區(qū)buffer中還未處理的數(shù)據(jù)后拋出異常
*/
output.write(buffer, 0, pad);
output.flush();
total += pad;
throw new MalformedStreamException
("Stream ended unexpectedly ");
}
}
}
output.flush();
return total;
}
9. discardBodyData方法
MultipartStream類中的discardBodyData方法用來跳過主體數(shù)據(jù),它與readBodyData方法非常相似,不同之處在于readBodyData方法把數(shù)據(jù)寫
入到一個輸出流中,而discardBodyData方法是把數(shù)據(jù)丟棄掉。discardBodyData方法返回被丟掉的字節(jié)個數(shù),方法調(diào)用完成后成員變量head指
向下一個分區(qū)分隔界線的首字節(jié)。MultipartStream類中定義discardBodyData這個方法,是為了忽略主體內(nèi)容部分的第一個分隔界線前面的內(nèi)
容,按照MIME規(guī)范,消息頭和消息體之間的分隔界線前面可以有一些作為注釋信息的內(nèi)容,discardBodyData就是為了拋棄這些注釋信息而提供
的。discardBodyData方法的源代碼如下:
public int discardBodyData() throws MalformedStreamException,IOException
{
boolean done = false;
int pad;
int pos;
int bytesRead;
int total = 0;
while (!done)
{
pos = findSeparator();
if (pos != -1)
{
total += pos - head;
head = pos;
done = true;
}
else
{
if (tail - head > keepRegion)
{
pad = keepRegion;
}
else
{
pad = tail - head;
}
total += tail - head - pad;
System.arraycopy(buffer, tail - pad, buffer, 0, pad);
head = 0;
bytesRead = input.read(buffer, pad, bufSize - pad);
if (bytesRead != -1)
{
tail = pad + bytesRead;
}
else
{
total += pad;
throw new MalformedStreamException
("Stream ended unexpectedly ");
}
}
}
return total;
}
10. readBoundary方法
對于圖1.3中的每一個分區(qū)的解析處理,程序首先要調(diào)用readHeaders方法讀取描述頭,接著要調(diào)用readBodyData(OutputStream output)讀取主
體數(shù)據(jù),這樣就完成了一個分區(qū)的解析。readBodyData方法內(nèi)部調(diào)用findSeparator方法找到了分隔界線,然后讀取分隔界線前面的內(nèi)容,此時
MultipartStream類中的成員變量head指向了buffer緩沖區(qū)中的分隔界線boundary的第一個字節(jié)。findSeparator方法只負責(zé)尋找分隔界線
boundary在緩沖區(qū)buffer中的位置,不負責(zé)從buffer緩沖區(qū)中讀走分隔界線的字節(jié)數(shù)據(jù)。在調(diào)用readBodyData方法之后,程序接著應(yīng)該讓成員
變量head跳過分隔界線,讓它指向下一個分區(qū)的描述頭的第一個字節(jié),才能調(diào)用readHeaders方法去讀取下一個分區(qū)的描述頭。
MultipartStream類中定義了一個readBoundary方法,用于讓成員變量head跳過分隔界線,讓它指向下一個分區(qū)的描述頭的第一個字節(jié)。對于圖
1.3中的最后的分隔界線,它比其他的分隔界線后面多了兩個“-”字符,而其他分隔界線與下一個分區(qū)的內(nèi)容之間還有一個回車換行,所以,
readBoundary方法內(nèi)部跳過分隔界線后,還需要再讀取兩個字節(jié)的數(shù)據(jù),才能讓成員變量head指向下一個分區(qū)的描述頭的第一個字節(jié)。
readBoundary方法內(nèi)部讀取分隔界線后面的兩個字節(jié)數(shù)據(jù)后,根據(jù)它們是回車換行、還是兩個“-”字符,來判斷這個分隔界線是下一個分區(qū)的
開始標記,還是整個請求消息的實體內(nèi)容的結(jié)束標記。如果readBoundary方法發(fā)現(xiàn)分隔界線是下一個分區(qū)的開始標記,那么它返回true,否則
返回false。readBoundary()方法的源代碼如下:
public boolean readBoundary()throws MalformedStreamException
{
byte[] marker = new byte[2];
boolean nextChunk = false;
head += boundaryLength; // 跳過分隔界線符
try
{
marker[0] = readByte();
marker[1] = readByte();
// 靜態(tài)常量STREAM_TERMINATOR ={‘-’、‘-’}
if (arrayequals(marker, STREAM_TERMINATOR, 2))
{
nextChunk = false;
}
// 靜態(tài)常量FIELD_SEPARATOR ={‘/n’、‘/r’}
else if (arrayequals(marker, FIELD_SEPARATOR, 2))
{
nextChunk = true;
}
else
{
/*如果讀到的既不是回車換行,又不是兩個減號,
說明輸入流有問題,則拋異常。
posted on 2008-05-08 14:38 gembin 閱讀(7377) 評論(1) 編輯 收藏 所屬分類: JavaEE