第1章文件上傳組件的應用與編寫
在許多Web站點應用中都需要為用戶提供通過瀏覽器上傳文檔資料的功能,例如,上傳郵件附件、個人相片、共享資料等。對文件上傳功能,在瀏覽器端提供了較好的支持,只要將FORM表單的enctype屬性設置為“multipart/form-data”即可;但在Web服務器端如何獲取瀏覽器上傳的文件,需要進行復雜的編程處理。為了簡化和幫助Web開發人員接收瀏覽器上傳的文件,一些公司和組織專門開發了文件上傳組件。本章將詳細介紹如何使用Apache文件上傳組件,以及分析該組件源程序的設計思路和實現方法。
1.1 準備實驗環境
按下面的步驟為本章的例子程序建立運行環境:
(1)在Tomcat 5.5.12的<tomcat的安裝目錄>\webapps目錄中創建一個名為fileupload的子目錄,并在fileupload目錄中創建一個名為test.html的網頁文件,在該文件中寫上“這是test.html頁面的原始內容!”這幾個字符。
(2)在<tomcat的安裝目錄>\webapps\fileupload目錄中創建一個名為WEB-INF的子目錄,在WEB-INF目錄中創建一個名為classes的子目錄和一個web.xml文件,web.xml文件內容如下:
<web-app>
</web-app>
(3)要使用Apache文件上傳組件,首先需要安裝Apache文件上傳組件包。在<tomcat的安裝目錄>\webapps\fileupload\WEB-INF目錄中創建一個名為lib的子目錄,然后從網址http://jakarta.apache.org/commons/fileupload下載到Apache組件的二進制發行包,在本書的附帶帶光盤中也提供了該組件的二進制發行包,文件名為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目錄中創建一個名為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環境變量中和確保編譯后生成的class文件存放到<tomcat安裝目錄>\webapps\fileupload\WEB-INF\classes目錄中,上面的CLASSPATH環境變量的設置值由于排版原因進行了換行,實際上不應該有換行。接著在src目錄中為compile.bat文件創建一個快捷方式,以后只要在Windows資源管理器窗口中將Java源文件拖動到compile.bat文件的快捷方式上,就可以完成Java源程序的編譯了。之所以要創建compile.bat文件的快捷方式,是因為直接將Java源程序拖動到compile.bat批處理文件時,compile.bat批處理文件內編寫的相對路徑不被支持。創建完的fileupload目錄中的文件結構如圖1.1所示。
圖1.1
(4)啟動Tomcat,在本地計算機的瀏覽器地址欄中輸入如下地址:
http://localhost:8080/fileupload/test.html
驗證瀏覽器能夠成功到該網頁文檔。如果瀏覽器無法訪問到該網頁文檔,請檢查前面的操作步驟和改正問題,直到瀏覽器能夠成功到該網頁文檔為止。
(5)為了讓/fileupload這個WEB應用程序能自動重新裝載發生了修改的Servlet程序,需要修改Tomcat的server.xml文件,在該文件的<Host>元素中增加如下一個<Context>子元素:
<Context path="/fileupload" docBase="fileupload" reloadable="true"/>
保存server.xml文件后,重新啟動Tomcat。
1.2 Apache文件上傳組件的應用
Java Web開發人員可以使用Apache文件上傳組件來接收瀏覽器上傳的文件,該組件由多個類共同組成,但是,對于使用該組件來編寫文件上傳功能的Java Web開發人員來說,只需要了解和使用其中的三個類:DiskFileUpload、FileItem和FileUploadException。這三個類全部位于org.apache.commons.fileupload包中。
1.2.1查看API文檔
在準備實驗環境時獲得的commons-fileupload-1.0.zip文件的解壓縮目錄中可以看到一個docs的子目錄,其中包含了Apache文件上傳組件中的各個API類的幫助文檔,從這個文檔中可以了解到各個API類的使用幫助信息。打開文件上傳組件API幫助文檔中的index.html頁面,在左側分欄窗口頁面中列出了文件上傳組件中的各個API類的名稱,在右側分欄窗口頁面的底部列出了一段示例代碼,如圖1.2所示。
圖1.2
讀者不需要逐個去閱讀圖1.2中列出的各個API類的幫助文檔,而應該以圖1.2中的示例代碼為線索,以其中所使用到的類為入口點,按圖索驥地進行閱讀,對于示例代碼中調用到的各個API類的方法則應重點掌握。
1.2.2 DiskFileUpload類
DiskFileUpload類是Apache文件上傳組件的核心類,應用程序開發人員通過這個類來與Apache文件上傳組件進行交互。下面介紹DiskFileUpload類中的幾個常用的重要方法。
1.setSizeMax方法
setSizeMax方法用于設置請求消息實體內容的最大允許大小,以防止客戶端故意通過上傳特大的文件來塞滿服務器端的存儲空間,單位為字節。其完整語法定義如下:
public void setSizeMax(long sizeMax)
如果請求消息中的實體內容的大小超過了setSizeMax方法的設置值,該方法將會拋出FileUploadException異常。
2.setSizeThreshold方法
Apache文件上傳組件在解析和處理上傳數據中的每個字段內容時,需要臨時保存解析出的數據。因為Java虛擬機默認可以使用的內存空間是有限的(筆者測試不大于100M),超出限制時將會發生“java.lang.OutOfMemoryError”錯誤,如果上傳的文件很大,例如上傳800M的文件,在內存中將無法保存該文件內容,Apache文件上傳組件將用臨時文件來保存這些數據;但如果上傳的文件很小,例如上傳600個字節的文件,顯然將其直接保存在內存中更加有效。setSizeThreshold方法用于設置是否使用臨時文件保存解析出的數據的那個臨界值,該方法傳入的參數的單位是字節。其完整語法定義如下:
public void setSizeThreshold(int sizeThreshold)
3. setRepositoryPath方法
setRepositoryPath方法用于設置setSizeThreshold方法中提到的臨時文件的存放目錄,這里要求使用絕對路徑。其完整語法定義如下:
public void setRepositoryPath(String repositoryPath)
如果不設置存放路徑,那么臨時文件將被儲存在"java.io.tmpdir"這個JVM環境屬性所指定的目錄中,tomcat 5.5.9將這個屬性設置為了“<tomcat安裝目錄>/temp/”目錄。
4. parseRequest方法
parseRequest 方法是DiskFileUpload類的重要方法,它是對HTTP請求消息進行解析的入口方法,
如果請求消息中的實體內容的類型不是“multipart/form-data”,該方法將拋出
FileUploadException異常。parseRequest 方法解析出FORM表單中的每個字段的數據,并將它們分別包裝成獨立的FileItem對象,然后將這些FileItem對象加入進一個List類型的集合對象中返回。parseRequest 方法的完整語法定義如下:public List parseRequest(HttpServletRequest req)
parseRequest 方法還有一個重載方法,該方法集中處理上述所有方法的功能,其完整語法定義如下:
parseRequest(HttpServletRequest req,int sizeThreshold,long sizeMax,
String path)
這兩個parseRequest方法都會拋出FileUploadException異常。
5. isMultipartContent方法
isMultipartContent方法方法用于判斷請求消息中的內容是否是“multipart/form-data”類型,是則返回true,否則返回false。isMultipartContent方法是一個靜態方法,不用創建DiskFileUpload類的實例對象即可被調用,其完整語法定義如下:
public static final boolean isMultipartContent(HttpServletRequest req)
6. setHeaderEncoding方法
由于瀏覽器在提交FORM表單時,會將普通表單中填寫的文本內容傳遞給服務器,對于文件上傳字段,除了傳遞原始的文件內容外,還要傳遞其文件路徑名等信息,如后面的圖1.3所示。不管FORM表單采用的是“application/x-www-form-urlencoded”編碼,還是“multipart/form-data”編碼,它們僅僅是將各個FORM表單字段元素內容組織到一起的一種格式,而這些內容又是由某種字符集編碼來表示的。關于瀏覽器采用何種字符集來編碼FORM表單字段中的內容,請參看筆者編著的《深入體驗java Web開發內幕——核心基礎》一書中的第6.9.2的講解,“multipart/form-data”類型的表單為表單字段內容選擇字符集編碼的原理和方式與“application/x-www-form-urlencoded”類型的表單是相同的。FORM表單中填寫的文本內容和文件上傳字段中的文件路徑名在內存中就是它們的某種字符集編碼的字節數組形式,Apache文件上傳組件在讀取這些內容時,必須知道它們所采用的字符集編碼,才能將它們轉換成正確的字符文本返回。
對于瀏覽器上傳給WEB服務器的各個表單字段的描述頭內容,Apache文件上傳組件都需要將它們轉換成字符串形式返回,setHeaderEncoding 方法用于設置轉換時所使用的字符集編碼,其原理與筆者編著的《深入體驗java Web開發內幕——核心基礎》一書中的第6.9.4節講解的ServletRequest.setCharacterEncoding方法相同。setHeaderEncoding 方法的完整語法定義如下:
public void setHeaderEncoding(String encoding)
其中,encoding參數用于指定將各個表單字段的描述頭內容轉換成字符串時所使用的字符集編碼。
注意:如果讀者在使用Apache文件上傳組件時遇到了中文字符的亂碼問題,一般都是沒有正確調用setHeaderEncoding方法的原因。
1.2.3 FileItem類
FileItem類用來封裝單個表單字段元素的數據,一個表單字段元素對應一個FileItem對象,通過調用FileItem對象的方法可以獲得相關表單字段元素的數據。FileItem是一個接口,在應用程序中使用的實際上是該接口一個實現類,該實現類的名稱并不重要,程序可以采用FileItem接口類型來對它進行引用和訪問,為了便于講解,這里將FileItem實現類稱之為FileItem類。FileItem類還實現了Serializable接口,以支持序列化操作。
對于“multipart/form-data”類型的FORM表單,瀏覽器上傳的實體內容中的每個表單字段元素的數據之間用字段分隔界線進行分割,兩個分隔界線間的內容稱為一個分區,每個分區中的內容可以被看作兩部分,一部分是對表單字段元素進行描述的描述頭,另外一部是表單字段元素的主體內容,如圖1.3所示。
圖 1.3
主體部分有兩種可能性,要么是用戶填寫的表單內容,要么是文件內容。FileItem類對象實際上就是對圖1.3中的一個分區的數據進行封裝的對象,它內部用了兩個成員變量來分別存儲描述頭和主體內容,其中保存主體內容的變量是一個輸出流類型的對象。當主體內容的大小小于DiskFileUpload.setSizeThreshold方法設置的臨界值大小時,這個流對象關聯到一片內存,主體內容將會被保存在內存中。當主體內容的數據超過DiskFileUpload.setSizeThreshold方法設置的臨界值大小時,這個流對象關聯到硬盤上的一個臨時文件,主體內容將被保存到該臨時文件中。臨時文件的存儲目錄由DiskFileUpload.setRepositoryPath方法設置,臨時文件名的格式為“upload_00000005(八位或八位以上的數字).tmp”這種形式,FileItem類內部提供了維護臨時文件名中的數值不重復的機制,以保證了臨時文件名的唯一性。當應用程序將主體內容保存到一個指定的文件中時,或者在FileItem對象被垃圾回收器回收時,或者Java虛擬機結束時,Apache文件上傳組件都會嘗試刪除臨時文件,以盡量保證臨時文件能被及時清除。
下面介紹FileItem類中的幾個常用的方法:
1. isFormField方法
isFormField方法用于判斷FileItem類對象封裝的數據是否屬于一個普通表單字段,還是屬于一個文件表單字段,如果是普通表單字段則返回true,否則返回false。該方法的完整語法定義如下:
public boolean isFormField()
2. getName方法
getName方法用于獲得文件上傳字段中的文件名,對于圖1.3中的第三個分區所示的描述頭,getName方法返回的結果為字符串“C:\bg.gif”。如果FileItem類對象對應的是普通表單字段,getName方法將返回null。即使用戶沒有通過網頁表單中的文件字段傳遞任何文件,但只要設置了文件表單字段的name屬性,瀏覽器也會將文件字段的信息傳遞給服務器,只是文件名和文件內容部分都為空,但這個表單字段仍然對應一個FileItem對象,此時,getName方法返回結果為空字符串"",讀者在調用Apache文件上傳組件時要注意考慮這個情況。getName方法的完整語法定義如下:
public String getName()
注意:如果用戶使用Windows系統上傳文件,瀏覽器將傳遞該文件的完整路徑,如果用戶使用Linux或者Unix系統上傳文件,瀏覽器將只傳遞該文件的名稱部分。
3.getFieldName方法
getFieldName方法用于返回表單字段元素的name屬性值,也就是返回圖1.3中的各個描述頭部分中的name屬性值,例如“name=p1”中的“p1”。getFieldName方法的完整語法定義如下:
public String getFieldName()
4. write方法
write方法用于將FileItem對象中保存的主體內容保存到某個指定的文件中。如果FileItem對象中的主體內容是保存在某個臨時文件中,該方法順利完成后,臨時文件有可能會被清除。該方法也可將普通表單字段內容寫入到一個文件中,但它主要用途是將上傳的文件內容保存在本地文件系統中。其完整語法定義如下:
public void write(File file)
5.getString方法
getString方法用于將FileItem對象中保存的主體內容作為一個字符串返回,它有兩個重載的定義形式:
public java.lang.String getString()
public java.lang.String getString(java.lang.String encoding)
throws java.io.UnsupportedEncodingException
前者使用缺省的字符集編碼將主體內容轉換成字符串,后者使用參數指定的字符集編碼將主體內容轉換成字符串。如果在讀取普通表單字段元素的內容時出現了中文亂碼現象,請調用第二個getString方法,并為之傳遞正確的字符集編碼名稱。
6. getContentType方法
getContentType 方法用于獲得上傳文件的類型,對于圖1.3中的第三個分區所示的描述頭,getContentType方法返回的結果為字符串“image/gif”,即“Content-Type”字段的值部分。如果FileItem類對象對應的是普通表單字段,該方法將返回null。getContentType 方法的完整語法定義如下:
public String getContentType()
7. isInMemory方法
isInMemory方法用來判斷FileItem類對象封裝的主體內容是存儲在內存中,還是存儲在臨時文件中,如果存儲在內存中則返回true,否則返回false。其完整語法定義如下:
public boolean isInMemory()
8. delete方法
delete
方法用來清空
FileItem類對象中存放的主體內容,如果主體內容被保存在臨時文件中,delete
方法將刪除該臨時文件。盡管Apache組件使用了多種方式來盡量及時清理臨時文件
,但系統出現異常時,仍有可能造成有的臨時文件被永久保存在了硬盤中。在有些情況下,可以調用這個方法來及時刪除臨時文件。其完整語法定義如下:public void
delete()
1.2.4 FileUploadException類
在文件上傳過程中,可能發生各種各樣的異常,例如網絡中斷、數據丟失等等。為了對不同異常進行合適的處理,Apache文件上傳組件還開發了四個異常類,其中FileUploadException是其他異常類的父類,
其他幾個類只是被間接調用的底層類,
對于Apache組件調用人員來說,只需對FileUploadException異常類進行捕獲和處理即可。1.2.5 文件上傳編程實例
下面參考圖1.2中看到的示例代碼編寫一個使用Apache文件上傳組件來上傳文件的例子程序。
:動手體驗:使用Apache文件上傳組件
(1)在<tomcat安裝目錄>\webapps\fileupload目錄中按例程1-1編寫一個名為FileUpload.html的HTML頁面,該頁面用于提供文件上傳的FORM表單,表單的enctype屬性設置值為“
multipart/form-data
”,表單的action屬性設置為“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創建一個名為UploadServlet.java的Servlet程序,UploadServlet.java調用Apache文件上傳組件來處理FORM表單提交的文件內容和普通字段數據。
例程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();
//設置保存上傳文件的目錄
String uploadDir = getServletContext().getRealPath("/upload");
if (uploadDir == null)
{
out.println("無法訪問存儲目錄!");
return;
}
File fUploadDir = new File(uploadDir);
if(!fUploadDir.exists())
{
if(!fUploadDir.mkdir())
{
out.println("無法創建存儲目錄!");
return;
}
}
if (!DiskFileUpload.isMultipartContent(request))
{
out.println("只能處理multipart/form-data類型的數據!");
return ;
}
DiskFileUpload fu = new DiskFileUpload();
//最多上傳200M數據
fu.setSizeMax(1024 * 1024 * 200);
//超過1M的字段數據采用臨時文件緩存
fu.setSizeThreshold(1024 * 1024);
//采用默認的臨時文件存儲位置
//fu.setRepositoryPath(...);
//設置上傳的普通字段的名稱和文件字段的文件名所采用的字符集編碼
fu.setHeaderEncoding("gb2312");
//得到所有表單字段對象的集合
List fileItems = null;
try
{
fileItems = fu.parseRequest(request);
}
catch (FileUploadException e)
{
out.println("解析數據時出現如下問題:");
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("存儲文件時出現如下問題:");
e.printStackTrace(out);
return;
}
finally //總是立即刪除保存表單字段內容的臨時文件
{
fi.delete();
}
}
}
//顯示處理結果
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()));
}
/**
*將一段字符串追加到一個結果字符串中。如果結果字符串的初始內容不為空,
*在追加當前這段字符串之前先最加一個逗號(,)。在組合sql語句的查詢條件時,
*經常要用到類似的方法,第一條件前沒有"and",而后面的條件前都需要用"and"
*作連詞,如果沒有選擇第一個條件,第二個條件就變成第一個,依此類推。
*
*@param result 要將當前字符串追加進去的結果字符串
*@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)單擊瀏覽器工具欄上的“后退”按鈕回到表單填寫頁面,只在第二個文件字段中選擇一個文件,單擊“上載”按鈕,瀏覽器返回的顯示結果如圖1.6所示。
圖1.6
M腳下留心:
上面編寫的Servlet程序將上傳的文件保存在了當前WEB應用程序下面的upload目錄中,這個目錄是客戶端瀏覽器可以訪問到的目錄。如果用戶通過瀏覽器上傳了一個名稱為test.jsp的文件,那么用戶接著就可以在瀏覽器中訪問這個test.jsp文件了,對于本地瀏覽器來說,這個jsp文件的訪問URL地址如下所示:
http://localhost:8080/fileupload/upload/test.jsp
對于遠程客戶端瀏覽器而言,只需要將上面的url地址中的localhost改寫為Tomcat服務器的主機名或IP地址即可。用戶可以通過上面的Servlet程序來上傳自己編寫的jsp文件,然后又可以通過瀏覽器來訪問這個jsp文件,如果用戶在jsp文件中編寫一些有害的程序代碼,例如,查看服務器上的所有目錄結構,調用服務器上的操作系統進程等等,這將是一個非常致命的安全漏洞和隱患,這臺服務器對外就沒有任何安全性可言了。
1.3 Apache文件上傳組件的源碼賞析
經常閱讀一些知名的開源項目的源代碼,可以幫助我們開闊眼界和快速提高編程能力。Apache文件上傳組件是Apache組織開發的一個開源項目,從網址http://jakarta.apache.org/commons/fileupload可以下載到Apache組件的源程序包,在本書的附帶帶光盤中也提供了該組件的源程序包,文件名為commons-fileupload-1.0-src.zip。該組件的設計思想和程序編碼細節包含有許多值得借鑒的技巧,為了便于有興趣的讀者學習和研究該組件的源碼,本節將分析Apache文件上傳組件的源代碼實現。對于只想了解如何使用Apache文件上傳組件來上傳文件的讀者來說,不必學習本節的內容。在學習本節內容之前,讀者需要仔細學習了筆者編著的《深入體驗java Web開發內幕——核心基礎》一書中的第6.7.2節中講解的“分析文件上傳的請求消息結構”的知識。
1.3.1 Apache文件上傳組的類工作關系
Apache文件上傳組件總共由兩個接口,十二個類組成。在Apache文件上傳組件的十二個類中,有兩個抽象類,四個的異常類,六個主要類,其中FileUpLoad類用暫時沒有應用,是為了以后擴展而保留的。Apache文件上傳組件中的各個類的關系如圖1.7所示,圖中省略了異常類。
圖 1.7
DiskFileUpload類是文件上傳組件的核心類,它是一個總的控制類,首先由Apache文件上傳組件的使用者直接調用DiskFileUpload類的方法,DiskFileUpload類再調用和協調更底層的類來完成具體的功能。解析類MultipartStream和工廠類DefaultFileItemFactory就是DiskFileUpload類調用的兩個的底層類。MultipartStream類用于對請求消息中的實體數據進行具體解析,DefaultFileItemFactory類對MultipartStream類解析出來的數據進行封裝,它將每個表單字段數據封裝成一個個的FileItem類對象,用戶通過FileItem類對象來獲得相關表單字段的數據。
DefaultFileItem是FileItem接口的實現類,實現了FileItem接口中定義的功能,用戶只需關心FileItem接口,通過FileItem接口來使用DefaultFileItem類實現的功能。DefaultFileItem類使用了兩個成員變量來分別存儲表單字段數據的描述頭和主體內容,其中保存主體內容的變量類型為DeferredFileOutputStream類。DeferredFileOutputStream類是一個輸出流類型,在開始時,DeferredFileOutputStream類內部使用一個ByteArrayOutputStream類對象來存儲數據,當寫入它里面的主體內容的大小大于DiskFileUpload.setSizeThreshold方法設置的臨界值時,DeferredFileOutputStream類內部創建一個文件輸出流對象來存儲數據,并將前面寫入到ByteArrayOutputStream類對象中的數據轉移到文件輸出流對象中。這個文件輸出流對象關聯的文件是一個臨時文件,它的保存路徑由DiskFileUpload.setRepositoryPath方法指定。
Apache文件上傳組件的處理流程如圖1.8所示。
圖1.8
圖1.8中的每一步驟的詳細解釋如下:
(1)Web容器接收用戶的HTTP請求消息,創建request請求對象。
(2)
調用
DiskFileUpload類對象的parseRequest方法對request請求對象進行解析。該方法首先檢查request請求對象中的數據內容
是否是“multipart/form-data”類型,如果是,
該方法則
創建MultipartStream類對象對request請求對象中的請求體 進行解析。(3)MultipartStream類對象對request請求體進行解析,并返回解析出的各個表單字段元素對應的內容。
(4)DiskFileUpload類對象的parseRequest方法接著創建DefaultFileItemFactory類對象,用來將MultipartStream類對象解析出的每個表單字段元素的數據封裝成FileItem類對象。
(5)DefaultFileItemFactory工廠類對象把MultipartStream類對象解析出的各個表單字段元素的數據封裝成若干DefaultFileItem類對象,然后加入到一個List類型的集合對象中,parseRequest方法返回該List集合對象。
實際上,步驟(3)和步驟(5)是交替同步進行的,即在MultipartStream類對象解析每個表單字段元素時,都會調用DefaultFileItemFactory工廠類把該表單字段元素封裝成對應的FileItem類對象。
1.3.2 Apache文件上傳組件的核心編程問題
WEB服務器端程序接收到
“multipart/form-data”類型的HTTP請求消息后,其核心和基本的編程工作就是讀取請求消息中的實體內容,然后解析出每個分區的數據,接著再從每個分區中解析出描述頭和主體內容部分。
在讀取HTTP請求消息中的實體內容時,只能調用HttpServletRequest.getInputStream方法返回的字節輸入流,而不能調用HttpServletRequest.getReader方法返回的字符輸入流,因為不管上傳的文件類型是文本的、還是其他各種格式的二進制內容,WEB服務器程序要做的工作就是將屬于文件內容的那部分數據原封不動地提取出來,然后原封不動地存儲到本地文件系統中。如果使用HttpServletRequest.getReader方法返回的字符輸入流對象來讀取HTTP請求消息中的實體內容,它將HTTP請求消息中的字節數據轉換成字符后再返回,這主要是為了方便要以文本方式來處理本來就全是文本內容的請求消息的應用,但本程序要求的是“原封不動”,顯然不能使用HttpServletRequest.getReader方法返回的字符輸入流對象來進行讀取。
另外,不能期望用一個很大的字節數組就可以裝進HTTP請求消息中的所有實體內容,因為程序中定義的字節數組大小總是有限制的,但應該允許客戶端上傳超過這個字節數組大小的實體內容。所以,只能創建一個一般大小的字節數組緩沖區來逐段讀取請求消息中的實體內容,讀取一段就處理一段,處理完上一段以后,再讀取下一段,如此循環,直到處理完所有的實體內容,如圖1.9所示。
圖 1.9
在圖1.9中,buffer即為用來逐段讀取請求消息中的實體內容的字節數組緩沖區。因為讀取到緩沖區中的數據處理完后就會被拋棄,確切地說,是被下一段數據覆蓋,所以,解析和封裝過程必須同步進行,程序一旦識別出圖1.3中的一個分區的開始后,就要開始將它封裝到一個FileItem對象中。
程序要識別出圖1.3中的每一個分區,需要在圖1.9所示的字節數組緩沖區buffer中尋找分區的字段分隔界線,當找到一個字段分隔界線后,就等于找到了一個分區的開始。筆者在《深入體驗java Web開發內幕——核心基礎》一書中的第6.7.2節中已經講過,上傳文件的請求消息的Content-Type頭字段中包含有用作字段分隔界線的字符序列,如下所示:
content-type : multipart/form-data; boundary=---------------------------7d51383203e8
顯然,我們可以通過調用HttpServletRequest.getHeader方法讀取Content-Type頭字段的內容,從中分離出分隔界線的字符序列,然后在字節數組緩沖區buffer中尋找分區的字段分隔界線。content-type頭字段的boundary參數中指定的字段分隔界線
是瀏覽器隨機產生的,瀏覽器保證它不會與用戶上傳的所有數據中的任何部分出現相同。
在這里有一點需要注意,圖1.3中的實體內容內部的字段分隔界線與content-type頭中指定的字段分隔界線有一點細微的差別,前者是在后者前面增加了兩個減號(-)字符而形成的,這倒不是什么編程難點。真正的編程難點在于在字節數組緩沖區buffer中尋找分隔界線時,可能會遇到字節數組緩沖區buffer中只裝入了分隔界線字符序列的部分內容的情況,如圖1.10所示。圖1.10
要解決這個問題的方法之一就是在查找字段分隔界線時,如果發現字節數組緩沖區buffer中只裝入了分隔界線字符序列的部分內容,那么就將這一部分內容留給字節數組緩沖區buffer的下一次讀取,如圖1.11所示。
圖1.11
這種方式讓字節數組緩沖區buffer下一次讀取的內容不是緊接著上一次讀取內容的后面,而是重疊上一次讀取的一部分內容,即從上一次讀取內容中的分隔界線字符序列的第一個字節處開始讀取。這種方式在實際的編程處理上存在著相當大的難度,程序首先必須確定字節數組緩沖區buffer上一次讀取的數據的后一部分內容正好是分隔界線字符序列的前面一部分內容,而這一部分內容的長度是不確定的,可能只是分隔界線字符序列的第一個字符,也可能是分隔界線字符序列的前面n-1個字符,其中n為分隔界線字符序列的整個長度。另外,即使確定字節數組緩沖區buffer上一次讀取的數據的后一部分內容正好是分隔界線字符序列的前面一部分內容,但它們在整個輸入字節流中的后續內容不一定就整個分隔界線字符序列的后一部分內容,出現這種情況的可能性是完全存在,程序必須進行全面和嚴謹的考慮。
Apache文件上傳組件的解決方法比較巧妙,它在查找字段分隔界線時,如果搜索到最后第n個字符時,n為分隔界線字符序列的長度,發現最后n個字符不能與分隔界線字符序列匹配,則將最后的n-1個字符留給字節數組緩沖區buffer的下一次讀取,程序再對buffer的下一次讀取的整個內容從頭開始查找字段分隔界線,如圖1.12所示。
圖1.12
Apache文件上傳組件查找字段分隔界線的具體方法,讀者可以請參見
MultipartStream
類的
findSeparator()方法中的源代碼。當找到一個分區的開始位置后,程序還需要分辨出分區中的描述頭和主體內容,并對這兩部分內容分開存儲。如何分辨出一個分區的描述頭和主體部分呢?從圖1.3中可以看到,每個分區中的描述頭和主體內容之間有一空行,再加上描述頭后面的換行,這就說明描述頭和主體部分之間是使用“\n”、“\r”、“\n”、“\r”這四個連續的字節內容進行分隔。因此,程序需要把“\n”、“\r”、“\n”、“\r”這四個連續的字節內容作為描述頭和主體部分之間的分隔界線,并在字節數組緩沖區buffer中尋找這個特殊的分隔界線來識別描述頭和主體部分。
當識別出一個分區中的描述頭和主體部分后,程序需要解決的下一個問題就是如何將描述頭和主體部分的數據保存到FileItem對象中,以便用戶以后可以調用FileItem類的方法來獲得這些數據。主體部分的數據需要能夠根據用戶上傳的文件大小有伸縮性地進行存儲,因此,程序要求編寫一個特殊的類來封裝主體部分的數據,對于這個問題的具體實現細節,讀者可參見1.2.4小節中講解的DeferredFileOutputStream類來了解。
1.3.3 MultipartStream類
MultipartStream
類用來對上傳的請求輸入流進行解析,它是整個
Apache上傳組件中最復雜的類。1.設計思想
MultipartStream類中定義了一個byte[]類型的boundary成員變量,這個成員變量用于保存圖1.3中的各個數據分區之間的分隔界線,每個分區分別代表一個表單字段的信息。圖1.3中的每個分區又可以分為描述頭部分和主體部分,
MultipartStream
類
中定義了一個readHeaders
()
方法來讀取描述頭部分的內容,
MultipartStream
類
中定義了一個readBodyData(OutputStream output)
方法來讀取主體部分的內容,并將這些內容寫入到一個作為參數傳入進來的輸出流對象中。readBodyData方法接收的參數output對象在應用中的實際類型是
DeferredFileOutputStream,這個對象又是保存在
DefaultFileItem類對象中的一個成員變量,這樣,readBodyData方法就可以將一個分區的主體部分的數據寫入到DefaultFileItem類對象中。因為圖1.3中的實體內容內部的字段分隔界線是在content-type頭中指定的字段分隔界線前面增加了兩個減號(-)字符而形成的,而每個字段分隔界線與它前面內容之間還進行了換行,這個換行并不屬于表單字段元素的內容。所以,MultipartStream類中的成員變量
boundary
中存儲的字節數組并不是直接從content-type頭的boundary參數中獲得的字符序列,
而是在boundary參數中指定的字符序列前面增加了四個字節,依次是‘\n’、‘\r’、‘-’和‘-’
。MultipartStream類中定義了一個readBoundary()方法來讀取和識別各個字段之間分隔界線,有一點特殊的是,
圖1.3中的第一個分隔界線前面沒有回車換行符,它是無法與
成員變量boundary
中的數據相匹配的,所以無法調用readBoundary()方法進行讀取,而是需要進行特殊處理,其后的每個分隔界線都與boundary中的數據相匹配,可以直接調用readBoundary()方法進行讀取。在本章的后面部分,
如果沒有特別說明,所說的
分隔界線都是指成員變量boundary中的數據內容。RFC 1867格式規范規定了描述頭和主體部分必須用一個空行進行分隔,
如圖1.3所示,也就是
描述頭和主體部分使用“\n”、“\r”、“\n”、“\r”這四個連續的字節內容進行分隔。
MultipartStream類的設計者為了簡化編程,在readHeaders
()
方法中
將“\n”、“\r”、“\n”、“\r”這四個連續的字節內容連同描述頭一起進行讀取。readHeaders()方法在讀取數據的過程中,當它發現第一個‘\n’、‘\r’、‘\n’、‘\r’ 連續的字節序列時就會返回,即使主體部分正好也包含了
“\n”、“\r”、“\n”、“\r”這四個連續的字節內容,但是,它們只會被隨后調用的
readBodyData方法作為主體內容讀取,永遠不會被readHeaders()方法讀取到,所以,它們不會與作為
描述頭和主體部分的分隔字符序列發生沖突。由于readHeaders()方法讀取了一個分區中的主體部分前面的所有內容(包括它前面的換行),而它與下一個分區之間的分隔界線前面的換行又包含在了成員變量
boundary
中,這個換行將被readBoundary
()方法讀取,所以,夾在readheaders()方法讀取的內容和readBoundary
()方法讀取的內容之間的數據全部都屬于表單字段元素的內容了,因此,讀取分區中的主體部分的readBodyData(OutputStream output)
方法不需要進行特別的處理,它直接將
讀取的數據寫入到DefaultFileItem類對象中封裝的DeferredFileOutputStream屬性對象中即可。2. 構造方法
MultipartStream類中的一個主要的構造方法的語法定義如下:
public (InputStream input, byte[] boundary, int bufSize)
其中,參數input是指從HttpServetRequest請求對象中獲得的字節輸入流對象,參數boundary是從請求消息頭中獲得的未經處理的分隔界線,bufSize指定圖1.10中的buffer緩沖區字節數組的長度,默認值是4096個字節。這個構造方法的源代碼如下:
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緩沖區中保留給下次讀取的最大字節個數
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; // 成員變量,表示正在處理的這個字節在buffer中的位置指針
tail = 0; // 成員變量,表示實際讀入到buffer中的字節個數
}
3. readByte方法
MultipartStream類中的readByte()方法從字節數組緩沖區buffer中讀一個字節,當buffer緩沖區中沒有更多的數據可讀時,該方法會自動從輸入流中讀取一批新的字節數據來重新填充buffer緩沖區。readByte()方法的源代碼如下:
public byte readByte () throws IOException
{
// 判斷是否已經讀完了buffer緩沖區中的所有數據
if (head == tail)
{
head = 0;
//讀入新的數據內容來填充buffer緩沖區
tail = input.read(buffer, head, bufSize);
if (tail == -1)
{
throw new IOException("No more data is available ");
}
}
return buffer[head++];// 返回當前字節,head++
}
其中,head變量是MultipartStream類中定義的一個int類型的成員變量,它用于表示正在讀取的字節在buffer數組緩沖區中的位置;tail變量也是MultipartStream類中定義的一個int類型的成員變量,它用于表示當前buffer數組緩沖區裝入的實際字節內容的長度。在MultipartStream類中主要是通過控制成員變量head的值來控制對buffer緩沖區中的數據的讀取和直接跳過某段數據,通過比較head與tail變量的值了解是否需要向buffer緩沖區中裝入新的數據內容。當每次向buffer緩沖區中裝入新的數據內容后,都應該調整成員變量head和tail的值。
4. arrayequals靜態方法
MultipartStream類中定義了一個的arrayequals靜態方法,用于比較兩個字節數組中的前面一部分內容是否相等,相等返回true,否則返回false。arrayequals方法的源代碼如下,參數count指定了對字節數組中的前面幾個字節內容進行比較:
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()方法從字節數組緩沖區buffer中的某個位置開始搜索一個特定的字節數據,如果找到了,則返回該字節在buffer緩沖區中的位置,不再繼續搜索,如果沒有找到,則返回-1。findByte方法的源代碼如下,參數pos制定了不搜索的起始位置值,value是要搜索的字節數據:
protected int findByte(byte value,int pos)
{
for (int i = pos; i < tail; i++)
{
if (buffer[i] == value)
{
return i; // 找到該值,findByte方法返回
}
}
return - 1;
}
如果程序需要在buffer緩沖區中多次搜索某個特定的字節數據,那就可以循環調用findByte方法,只是在每次調用findByte方法時,必須不斷地改變參數pos的值,讓pos的值等于上次調用findByte的返回值,直到findByte方法返回-1時為止,如圖1.13所示。
圖1.13
6. findSeparator方法
MultipartStream類中的findSeparator方法用于從字節數組緩沖區buffer中查找成員變量boundary中定義的
分隔界線
,并返回分隔界線的第一個字節在
buffer緩沖區中的位置,如果在buffer緩沖區中沒有找到分隔界線
,則返回-1。findSeparator方法內部首先調用findByte方法在buffer緩沖區中搜索分隔界線
boundary
的第一個字節內容,如果沒有找到,則說明
buffer緩沖區中沒有包含分隔界線;如果
findByte方法在buffer緩沖區中找到了分隔界線boundary
的第一個字節內容,
findSeparator方法內部接著確定該字節及隨后的字節序列是否確實是分隔界線。
findSeparator方法內部循環調用findByte方法,直到找到分隔界線或者findByte方法已經查找到了
buffer緩沖區中的最后boundaryLength -1
個字節。
findSeparator方法內部為什么調用findByte
方法查找到
buffer緩沖區中的最后boundaryLength-1
個字節時就停止查找呢?這是為了解決如
圖1.10所示的buffer緩沖區中裝入了分隔界線的部分內容的特殊情況,所以在
findSeparator()方法中不要搜索buffer緩沖區中的最后的boundaryLength -1
個字節,而是把
buffer緩沖區中的最后這boundaryLength -1
個字節作為保留區,在下次讀取buffer緩沖區時將這些保留的字節數據重新填充到buffer緩沖區的開始部分
。findSeparator方法的源代碼如下:protected int findSeparator()
{
int first;
int match = 0;
int maxpos = tail - boundaryLength;//在buffer中搜索的最大位置
for (first = head;(first <= maxpos) && (match != boundaryLength);
first++)
{
//在buffer緩沖區中尋找boundary的第一個字節
first = findByte(boundary[0], first);
/*buffer中找不到boundary[0]或者boundary[0]位于保留區中,
則可以判斷buffer中不存在分隔界線*/
if (first == -1 || (first > maxpos))
{
return -1;
}
//確定隨后的字節序列是否確實是分隔界線的其他字節內容
for (match = 1; match < boundaryLength; match++)
{
if (buffer[first + match] != boundary[match])
{
break;
}
}
}
// 當前buffer中找到boundary,返回第一個字節所在位置值
if (match == boundaryLength)
{
return first - 1;
}
return -1; // 當前buffer中沒找到boundary,返回-1
}
圖1.14中描述了findSeparator方法內部定義的各個變量的示意圖。
圖1.14
findSeparator方法內部的代碼主要包括如下三個步驟:
(1)循環調用findByte(boundary[0], first)找到buffer緩沖區中的與boundary[0]相同的字節的位置,并將位置記錄在first變量中。
(2)比較buffer緩沖區中的first后的boundaryLength
-1
個字節序列是否與boundary中的其他字節序列相同。如果不同,說明這個first變量指向的字節不是分隔界線的開始字節,跳出內循環,將first變量加1后繼續外循環
調用findByte方法;如果相同,說明在當前緩沖區buffer中找到了分隔界線,
內循環正常結束,此時
match變量的值為boundaryLength,接著執行外循環將first變量
加1,然后執行外循環的條件判斷,由于match != boundaryLength條件不成立,外循環也隨之結束。(3)判斷match是否等于boundaryLength,如果等于則說明找到了分隔界線,此時返回成員變量
boundary
的第一個字節在緩沖區buffer中位置,由于第(2)中將first加1了,所以這里的返回值應該是
first-1;如果不等,說明當前緩沖區huffer中沒有分隔界線,
返回-1。7. readHeaders方法
MultipartStream類中的readHeaders方法用于讀取一個分區的描述頭部分,并根據DiskFileUpload類的setHeaderEncoding方法設定的字符集編碼將描述頭部分轉換成一個字符串返回。
在調用readHeaders方法之前時,程序已經調用了findSeparator方法找到了分隔界線和讀取了分隔界線前面的內容,此時MultipartStream類中的成員變量head指向了buffer緩沖區中的分隔界線
boundary
的第一個字節,程序接著應調用
readBoundary方法跳過
分隔界線及其隨后的回車換行兩個字節,以保證在調用readHeaders方法時,成員變量head已經指向了分區的描述頭的第一個字節。在readHeaders方法內部,直接循環調用readByte方法讀取字節數據,并把讀到的數據存儲在一個字節數組輸出流中,直到讀取到了連續的兩次回車換行字符,就認為已經讀取完了描述頭的全部內容,此時成員變量head將指向分區中的主體內容的第一個字節。readHeaders()方法的源代碼如下:public String readHeaders()throws MalformedStreamException
{
int i = 0;
//從下面的代碼看來,這里定義成一個byte即可,不用定義成byte數組
byte b[] = new byte[1];
//用于臨時保存描述頭信息的字節數組輸出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//對描述頭部分的數據內容過大進行限制處理
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++;
//靜態常量HEADER_SEPARATOR的值為:{0x0D, 0x0A, 0x0D, 0x0A}
if (b[0] == HEADER_SEPARATOR[i])
{
i++;
}
else
{
i = 0;
}
if (size <= sizeMax)
{
baos.write(b[0]); // 將當前字節存入緩沖流
}
}
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方法循環調用readByte()方法逐個讀取buffer緩沖區中的字節,并將讀取的字節與HEADER_SEPARATOR ={‘\n’,‘\r’,‘\n’,‘\r’}的第一個字節進行比較,如果這個字節等于HEADER_SEPARATOR的首字節‘\n’,則循環控制因子i加1,這樣,下次調用readByte()方法讀取的字節將與HEADER_SEPARATOR中的第二字節比較,如果相等,則依照這種方式比較后面的字節內容,如果連續讀取到了HEADER_SEPARATOR字節序列,則循環語句結束。readHeaders方法將讀取到的每個正常字節寫入到了一個字節數組輸出流中,其中也包括作為描述頭與主體內容之間的分隔序列HEADER_SEPARATOR中的字節數據。由于readByte()方法會自動移動head變量的值和自動向緩沖區buffer中載入數據,所以,readHeaders方法執行完以后,成員變量head指向分區主體部分的首字節。readHeaders方法最后將把存入字節數組輸出流中的字節數據按指定字符集編碼轉換成字符串并返回,該字符串就是描述頭字符串。
8. readBodyData方法
MultipartStream類中的readBodyData方法用于把主體部分的數據寫入到一個輸出流對象中,并返回寫入到輸出流中的字節總數。當調用readBodyData方法前,成員變量head已經指向了分區的主體部分的首字節,readBodyData方法調用完成后,成員變量head指向分區分隔界線的首字節。readBodyData方法中需要調用findSeparator方法找出下一個分區分隔界線的首字節位置,才能知道這次讀取的分區主體內容的結束位置。從分區主體部分的首字節開始,直到在findSeparator方法找到的下一個分區分隔界線前的所有數據都是這個分區的主體部分的數據,readBodyData方法需要把這些數據都寫到輸出流output對象中。如果findSeparator方法在buffer緩沖區中沒有找到分區分隔界線,readBodyData方法還必須向buffer緩沖區中裝入新的數據內容后繼續調用findSeparator方法進行處理。在向buffer緩沖區中裝入新的數據內容時,必須先將上次保留在buffer緩沖區中的內容轉移進新buffer緩沖區的開始處。readBodyData方法的源代碼如下,傳遞給readBodyData方法的參數實際上是一個DeferredFileOutputStream類對象:
public int readBodyData(OutputStream output)
throws MalformedStreamException,IOException
{
// 用于控制循環的變量
boolean done = false;
int pad;
int pos;
int bytesRead;
// 寫入到輸出流中的字節個數
int total = 0;
while (!done)
{
pos = findSeparator();// 搜索分隔界線
if (pos != -1) //緩沖區buffer中包含有分隔界線
{
output.write(buffer, head, pos - head);
total += pos - head;
head = pos;//head變量跳過主體數據,指向分隔界線的首字節
done = true;// 跳出循環
}
else //緩沖區buffer中沒有包含分隔界線
{
/*根據緩沖區中未被readHeaders方法讀取的數據內容是否大于圖1.4中的
保留區的大小,來決定保留到下一次buffer緩沖區中的字節個數
*/
if (tail - head > keepRegion)
{
pad = keepRegion;
}
else
{
pad = tail - head;
}
output.write(buffer, head, tail - head - pad);
total += tail - head - pad;//統計寫入到輸出流中的字節個數
/*將上一次buffer緩沖區中的未處理的數據轉移到
下一次buffer緩沖區的開始位置
*/
System.arraycopy(buffer, tail - pad, buffer, 0, pad);
head = 0; //讓head變量指向緩沖區的開始位置
//向buffer緩沖區中載入新的數據
bytesRead = input.read(buffer, pad, bufSize - pad);
if (bytesRead != -1)
{
//設置buffer緩沖區中的有效字節的個數
tail = pad + bytesRead;
}
else
{
/*還沒有找到分隔界線,輸入流就結束了,輸入流中的數據格式
顯然不正確,保存緩沖區buffer中還未處理的數據后拋出異常
*/
output.write(buffer, 0, pad);
output.flush();
total += pad;
throw new MalformedStreamException
("Stream ended unexpectedly ");
}
}
}
output.flush();
return total;
}
9. discardBodyData方法
MultipartStream類中的discardBodyData方法用來跳過主體數據,它與readBodyData方法非常相似,不同之處在于readBodyData方法把數據寫入到一個輸出流中,而discardBodyData方法是把數據丟棄掉。discardBodyData方法返回被丟掉的字節個數,方法調用完成后成員變量head指向下一個分區分隔界線的首字節。MultipartStream類中定義discardBodyData這個方法,是為了忽略主體內容部分的第一個分隔界線前面的內容,按照MIME規范,消息頭和消息體之間的分隔界線前面可以有一些作為注釋信息的內容,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中的每一個分區的解析處理,程序首先要調用readHeaders方法讀取描述頭,接著要調用readBodyData(OutputStream output)讀取主體數據,這樣就完成了一個分區的解析。readBodyData方法內部調用findSeparator方法找到了分隔界線,然后讀取分隔界線前面的內容,此時MultipartStream類中的成員變量head指向了buffer緩沖區中的分隔界線
boundary
的第一個字節。
findSeparator方法只負責尋找分隔界線boundary在緩沖區buffer中的位置,不負責從buffer緩沖區中讀走分隔界線的字節數據。在調用readBodyData方法之后,程序接著應該讓
成員變量head跳過
分隔界線,讓它指向下一個分區的描述頭的第一個字節,才能調用readHeaders方法去讀取下一個分區的描述頭。MultipartStream類中定義了一個readBoundary方法,用于讓成員變量head跳過分隔界線,讓它指向下一個分區的描述頭的第一個字節。對于圖1.3中的最后的分隔界線,它比其他的分隔界線后面多了兩個“-”字符,而其他分隔界線與下一個分區的內容之間還有一個回車換行,所以,readBoundary方法內部跳過分隔界線后,還需要再讀取兩個字節的數據,才能讓成員變量head指向下一個分區的描述頭的第一個字節。readBoundary方法內部讀取分隔界線后面的兩個字節數據后,根據它們是回車換行、還是兩個“-”字符,來判斷這個分隔界線是下一個分區的開始標記,還是整個請求消息的實體內容的結束標記。如果readBoundary方法發現分隔界線是下一個分區的開始標記,那么它返回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();
// 靜態常量STREAM_TERMINATOR ={‘-’、‘-’}
if (arrayequals(marker, STREAM_TERMINATOR, 2))
{
nextChunk = false;
}
// 靜態常量FIELD_SEPARATOR ={‘/n’、‘/r’}
else if (arrayequals(marker, FIELD_SEPARATOR, 2))
{
nextChunk = true;
}
else
{
/*如果讀到的既不是回車換行,又不是兩個減號,
說明輸入流有問題,則拋出異常。*/
throw new MalformedStreamException(
" Unexpected characters follow a boundary " );
}
}
catch (IOException e)
{
throw new MalformedStreamException("Stream ended unexpectedly ");
}
return nextChunk;
}
11. skipPreamble方法
在本節開始部分已經分析過,因為圖1.3中的實體內容內部的字段分隔界線是在content-type頭中指定的字段分隔界線前面增加了兩個減號(-)字符而形成的,而每個字段分隔界線與它前面內容之間還進行了換行,這個換行并不屬于表單字段元素的內容,所以,MultipartStream類中的成員變量
boundary
中存儲的字節數組并不是直接從content-type頭的boundary參數中獲得的字符序列,
而是在boundary參數中指定的字符序列前面增加了四個字節,依次是‘\n’、‘\r’、‘-’和‘-’
。readBoundary方法根據成員變量boundary中的內容來識別和跳過各個字段之間的分隔界線,但是,圖1.3中的第一個分隔界線前面沒有回車換行符,它是無法與
成員變量boundary
中的數據相匹配的,所以無法調用readBoundary()方法來跳過第一個分隔界線。
MultipartStream類中定義的skipPreamble方法專門用于跳過第一個分隔界線。skipPreamble方法內部的編寫思路如下:
(1)首先修改成員變量
boundary
中的數據內容
,去掉前面的‘\n’和‘\r’這兩個字節
,這樣,成員變量boundary
中的數據內容就可以與第一個
分隔界線想匹配了。(2)接著就可以調用readBoundary方法跳過
第一個分隔界線
。(3)跳過
第一個分隔界線后,
skipPreamble方法又恢復成員變量boundary
中的原始數據,以便以后調用的readBoundary方法可以正常工作
。skipPreamble()方法的源代碼如下:
public boolean skipPreamble()throws IOException
{
//修改成員變量boundary和boundaryLength的值
System.arraycopy(boundary, 2, boundary, 0, boundary.length - 2);
boundaryLength = boundary.length - 2;
try
{
//丟掉第一個分隔界線符前的注釋數據
discardBodyData();
return readBoundary();
}
catch (MalformedStreamException e)
{
return false;
}
finally
{
//恢復成員變量boundary和boundaryLength的值
System.arraycopy(boundary, 0, boundary, 2, boundary.length - 2);
boundaryLength = boundary.length;
boundary[0] = 0x0D; //‘\n’
boundary[1] = 0x0A; // ‘\r’
}
}
12. MultipartStream類的綜合應用
了解了MultipartStream類中的各個方法的作用與工作原理后,我們就可以直接調用MultipartStrean類中的這些方法來獲取瀏覽器
上傳的文件內容了
。下面列舉出一段簡單的示意代碼,以幫助讀者更好地了解和掌握MultipartStream類的各個方法。為了讓代碼簡單易讀,下面的代碼沒有進行異常處理和沒有考慮非文件字段的情況。public void doPost(HttpServletRequest req, HttpServletResponse res)
{
//從請求消息中獲得content-type頭字段
String content_type = req.getHeader("content-type");
byte [] boundary = //從content_type中分離出分隔界線
//從請求消息中獲得用于讀取實體內容的輸入流對象
InputStream input = req.getInputStream();
//構建MultipartStream對象
MultipartStream ms = new MultipartStream(input,boundary);
//跳過第一個分隔界線
boolean nexPart = ms.skipPreamble();
//循環處理每個分區
while(nexPart)
{
String header = ms.readHeaders()
String file = //從header中提取出文件名
FileOutputStream fos = new FileOutputStream(file);
//將分區的主體內容直接寫入到一個文件中
ms.readBodyData(fos);
//跳過下一個分隔界線,并返回是否是最后一個分隔界線
nexPart = ms.readBoundary();
}
}
1.3.4 DeferredFileOutputStream類
DeferredFileOutputStream類用來封裝MultipartStream類解析出的各個分區的主體數據,它繼承了抽象類ThresholdingOutputStream。DeferredFileOutputStream類的內部在開始時使用ByteArrayOutputStream類對象存儲主體數據,當其存儲的數據量超過一個限定值時,它就會創建一個臨時文件來儲存所有的主體數據。
1.構造方法
DeferredFileOutputStream類中的一個主要的構造方法的源代碼如下:
public DeferredFileOutputStream(int threshold, File outputFile)
{
//設置轉換存儲方式的臨界值
super(threshold);
//設置儲存主體數據的臨時文件
this.outputFile = outputFile;
memoryOutputStream = new ByteArrayOutputStream(threshold);
//記錄當前正使用的輸出流對象
currentOutputStream = memoryOutputStream;
}
DeferredFileOutputStream類的父類ThresholdingOutputStream的構造方法的源代碼如下:
public ThresholdingOutputStream(int threshold)
{
this.threshold = threshold;
}
2. getData方法
DeferredFileOutputStream類中的getData方法用于把存儲在ByteArrayOutputStream類型的memoryOutputStream對象中的主體數據以字節數組的形式返回,如果主體數據當前是存儲在FileOutputStream類型的diskOutputStream對象中,這個方法返回null。getData方法的源代碼如下:
public byte[] getData()
{
if (memoryOutputStream != null)
{
return memoryOutputStream.toByteArray();
}
return null;
}
3. getFile方法
DeferredFileOutputStream類中的getFile方法返回代表臨時文件的File類型的outputFile成員變量的引用。getFile方法的源代碼如下:
public File getFile()
{
return outputFile;
}
4. getStream方法
DeferredFileOutputStream類中的getStream方法用于返回成員變量currentOutputStream的引用,即返回當前正用于存儲主體數據的輸出流對象。getStream方法的源代碼如下:
protected OutputStream getStream() throws IOException
{
return currentOutputStream;
}
5. isThresholdExceeded方法
DeferredFileOutputStream類中的isThresholdExceeded方法用于判斷保存在DeferredFileOutputStream對象中的主體數據是否超過了要轉換存儲方式的臨界值,它是從父類繼承的方法,其源代碼如下:
public boolean isThresholdExceeded()
{
return (written > threshold);
}
其中,written是一個int型的成員變量,表示當前已經寫入到DeferredFileOutputStream對象中的字節數,threshold表示要轉換存儲方式的那個臨界值。
6. isInMemory()方法
DeferredFileOutputStream類中的isInMemory方法用于檢測主體數據當前是否存儲在內存(memoryOutputStream對象)中,是則返回true,否則返回false。isInMemory方法的源代碼如下:
public boolean isInMemory()
{
return (!isThresholdExceeded());
}
7. thresholdReached()方法
DeferredFileOutputStream類中的thresholdReached方法用于把內存(memoryOutputStream對象)中的數據轉入到outputFile文件中,并切換到文件存儲模式狀態(讓currentOutputStream指向文件輸出流對象,將memoryOutputStream對象置為null)。thresholdReached方法的源代碼如下:
protected void thresholdReached() throws IOException
{
byte[] data = memoryOutputStream.toByteArray();
FileOutputStream fos = new FileOutputStream(outputFile);
fos.write(data);
diskOutputStream = fos; // 改變成員變量
currentOutputStream = fos; // 改變成員變量
memoryOutputStream = null; // 將對象置空
}
diskOutputStream是FileOutputStream類型的一個成員變量,表示臨時文件輸出流對象。
8. checkThreshold(int count)方法
DeferredFileOutputStream類中的checkThreshold方法從父類繼承而來,用于判斷如果寫入指定數量的字節,寫入到DeferredFileOutputStream對象中的總字節數是否會超過要轉換存儲方式的那個臨界值,如果是,則調用thresholdReached()方法把內存中的數據轉入到文件中。checkThreshold方法的源代碼如下:
protected void checkThreshold(int count) throws IOException
{
if (!thresholdExceeded && (written + count > threshold))
{
thresholdReached();
thresholdExceeded = true;
}
}
傳遞給checkThreshold方法的count參數必須大于1。thresholdExceeded是一個boolean類型的成員變量,表示寫入到DeferredFileOutputStream對象中的總字節數是否超過了要轉換存儲方式的那個臨界值,是則為true,否則為false。
9. write方法
DeferredFileOutputStream類中實現了OutputStream類中定義的多種重載形式的write方法,用于向DeferredFileOutputStream對象中寫入一個或多個字節,下面是兩個重載的write方法的源代碼:
public void write(int b) throws IOException
{
checkThreshold(1);
getStream().write(b);
written++;
}
public void write(byte b[]) throws IOException
{
checkThreshold(b.length);
getStream().write(b);
written += b.length;
}
從DeferredFileOutputStream類中實現的write方法的源代碼中可以看到,write方法在寫入一個或多個字節之前,都先要調用checkThreshold方法檢查是否要轉換存儲方式和完成存儲方式的轉換。
上面僅講解了一些能闡述清楚DeferredFileOutputStream類的工作機制的方法和將被DefaultFileItem類調用的主要方法,DeferredFileOutputStream類的其他方法請讀者自己參看其源文件。
1.3.5 DefaultFileItem類
DefaultFileItem類實現了FileItem接口,它用用封裝MultipartStream類解析出的一個分區的數據。DefaultFileItem類中定義了多個私有屬性來分別保存分區的描述頭部分的各個字段和參數的值,它還定義了一個DeferredFileOutputStream類型的成員變量dfos來保存分區的主體部分。
1.構造方法
DefaultFileItem類的構造方法的源代碼如下:
DefaultFileItem(String fieldName, String contentType, boolean isFormField,
String fileName, int sizeThreshold, File repository)
{
this.fieldName = fieldName;
this.contentType = contentType;
this.isFormField = isFormField;
this.fileName = fileName;
this.sizeThreshold = sizeThreshold;
this.repository = repository;
}
DefaultFileItem構造方法中所用到的各個成員變量的意義和作用如下:
(1)成員變量fieldName中保存了如圖1.3所示的Content-Disposition字段中的name參數的值,以后可以調用DefaultFileItem類的getFieldName()方法來獲取這個變量值;
(2)成員變量fileName中保存了如圖1.3所示的Content-Disposition字段中的filename參數的值,以后可以調用DefaultFileItem類的getName()方法來獲取這個變量值;
(3)成員變量contentType中保存了如圖1.3所示的contentType字段的值,以后可以調用DefaultFileItem類的getContentType()方法來獲取這個變量值;
(4)成員變量sizeThreshold用于設置DefaultFileItem中的DeferredFileOutputStream對象轉換存儲模式的臨界值,如果超過該值,則DeferredFileOutputStream對象將把主體數據轉入一個臨時文件中;
(5)成員變量isFormField用于說明當前分區內容是普通表單字段的信息,還是文件表單字段的信息,true表示當前分區內容是普通表單字段的信息,false表示當前分區內容是文件表單字段的信息。
(6)成員變量repository用于設置主體數據存入到的臨時文件的目錄。
2.delete方法
DefaultFileItem類的delete方法清空成員變量cachedContent,并刪除臨時文件,這個方法主要用于刪除因各種原因而沒有及時被刪除掉的臨時文件。delete方法的源代碼如下:
public void delete()
{
cachedContent = null;
File outputFile = getStoreLocation()
// 該方法返回臨時文件對應的File對象
if (outputFile != null && outputFile.exists())
{
outputFile.delete();
}
}
其中的cachedContent變量是一個byte數組類型的成員變量,它代表存儲在DeferredFileOutputStream對象中的ByteArrayOutputStream對象中的數據;outputFile變量代表臨時文件的File類對象。
3.finalize方法
DefaultFileItem類覆蓋了Object類的finalize方法,在finalize方法中刪除臨時文件,以增強清除臨時文件的保險系數。finalize方法的源代碼如下:
protected void finalize()
{
File outputFile = dfos.getFile(); // 返回臨時文件對應的File對象
if (outputFile != null && outputFile.exists())
{
outputFile.delete();
}
}
4. getSize方法
DefaultFileItem類的getSize方法返回DeferredFileOutputStream類型的成員變量dfos對象中保存的字節數,即返回分區主體部分的數據大小。getSize方法的源代碼如下:
public long getSize()
{
if (cachedContent != null)
{
return cachedContent.length;
}
else if (dfos.isInMemory())
{
return dfos.getData().length;
}
else
{
return dfos.getFile().length();
}
}
5. get方法
DefaultFileItem類的get方法以字節數組的形式,返回存儲在成員變量dfos對象(存儲分區主體部分的數據)中的所有內容。如果數據是存儲在臨時文件中,且文件中的數據量非常大,將會出現內存不夠的錯誤。get方法的源代碼如下:
public byte[] get()
{
if (dfos.isInMemory())
{
if (cachedContent == null)
{
cachedContent = dfos.getData();// 取出內存中所有數據
}
return cachedContent;
}
byte[] fileData = new byte[(int) getSize()];//取出臨時文件中數據
FileInputStream fis = null;
try
{
fis = new FileInputStream(dfos.getFile());
fis.read(fileData);
}
catch (IOException e)
{
fileData = null;
}
finally // 無論如何,關閉流
{
if (fis != null)
{
try
{
fis.close();
}
catch (IOException e)
{
// 不作任何處理
}
}
}
return fileData;
}
6. getInputStream方法
DefaultFileItem類的getInputStream方法以輸入流的形式返回存儲在成員變量dfos對象(存儲分區主體部分的數據)中的數據,它的源代碼如下:
public InputStream getInputStream() throws IOException
{
if (!dfos.isInMemory())
{
return new FileInputStream(dfos.getFile());
}
if (cachedContent == null)
{
cachedContent = dfos.getData();
}
return new ByteArrayInputStream(cachedContent);
}
7. getUniqueId()方法
DefaultFileItem類的getUniqueId方法返回一個至少包含八個字符的字符串,這個字符串被用作臨時文件名稱的標識部分。getUniqueId方法保證該字符串從WEB服務器啟動以來是唯一的,它的源代碼如下:
private static String getUniqueId()
{
int current;
// 同步處理,防止在多線程情況下出現臨時文件重名的現象
synchronized (DefaultFileItem.class)
{
current =counter++;
}
String id = Integer.toString(current);
if (current < 100000000)
{
//將id變成前面補0的八位字符串
id = (“00000000” + id).substring(id.length());
}
return id;
}
getUniqueId方法中兩個編程小技巧值得我們借鑒:
(1)為了保證id在多線程情況下的唯一性,getUniqueId方法中加入了同步代碼塊,其同步鎖對象選擇了DefaultFileItem.class,DefaultFileItem.class為代表DefaultFileItem類的字節碼的對象,它與靜態變量counter在當前類裝載器的名稱空間中都是唯一的,從而可以用作靜態變量counter的同步鎖對象。
(2)為了將變量id變成前面補0的八位字符串,getUniqueId方法使用了如下語句,真是有點巧妙:
id = (“00000000” + id).substring(id.length());
8. getTempFile方法
DefaultFileItem類的getTempFile方法返回一個代表臨時文件的File類對象,這個File類對象將被用來保存分區主體部分的數據。getTempFile方法的源代碼如下:
protected File getTempFile()
{
File tempDir = repository;
if (tempDir == null)
{
tempDir = new File(System.getProperty("java.io.tmpdir"));
}
String fileName = "upload_" + getUniqueId() + ".tmp";
File f = new File(tempDir, fileName);
f.deleteOnExit();
return f;
}
9. getOutputStream方法
DefaultFileItem類的getOutputStream方法返回DefaultFileItem對象中的用于保存分區主體數據的DeferredFileOutputStream類型的dfos成員變量的引用,它的源代碼如下:
public OutputStream getOutputStream()throws IOException
{
if (dfos == null)
{
File outputFile = getTempFile();// 產生一個唯一文件名的文件對象
dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
}
return dfos;
}
10.getString方法
DefaultFileItem類的getString方法以字符串的形式返回dfos對象中的所有內容,即以字符串形式返回分區的主體部分的數據。getString方法有兩種重載形式,它的源代碼如下:
public String getString(String encoding)
throws UnsupportedEncodingException
{
return new String(get(), encoding);
}
public String getString()
{
return new String(get());
}
11. write(File file)方法
DefaultFileItem類的write方法用于把dfos對象中的所有數據寫入到一個文件中。write方法在把dfos對象的數據寫入到指定的文件中時,采用的處理辦法也很巧妙,首先是把文件重命名成指定的文件名,如果重命名不成功,則按指定的文件名新建一個文件輸出流對象,再將dfos對象中的所有數據循環寫入到新建文件中。write方法的源代碼如下:
public void write(File file) throws Exception
{
if (isInMemory())// 主體數據存在內存中
{
FileOutputStream fout = null;
try
{
fout = new FileOutputStream(file);
fout.write(get());
}
finally // 無論任何都要關閉輸出流
{
if (fout != null)
{
fout.close();
}
}
}
else // 主體數據存儲在臨時文件中
{
//返回臨時文件對應的File對象
File outputFile = getStoreLocation();
if (outputFile != null)
{
//不能改文件名
if (!outputFile.renameTo(file))
{
BufferedInputStream in = null;
BufferedOutputStream out = null;
try
{
in = new BufferedInputStream(
new FileInputStream(outputFile));
out = new BufferedOutputStream(
new FileOutputStream(file));
byte[] bytes = new byte[2048];
int s = 0;
while ((s = in.read(bytes)) != -1)
{
out.write(bytes, 0, s);
}
}
finally
{
try
{
in.close();
}
catch (IOException e)
{
// 忽略
}
try
{
out.close();
}
catch (IOException e)
{
// ignore
}
}
}
}
else // outputFile = null,找不到指定文件
{
throw new FileUploadException(
"Cannot write uploaded file to disk! ");
}
}
}
1.3.6 DefaultFileItemFactory類
DefaultFileItemFactory類實現了FileItemFactory接口,該類負責創建DefaultFileItem類的實例對象。
1. 構造方法
DefaultFileItemFactory類的構造方法有如下兩種重載形式:
public DefaultFileItemFactory();
public DefaultFileItemFactory(int sizeThreshold,File repository);
在第二個構造方法中,參數sizeThreshold用于設置是否使用臨時文件保存解析出的數據的那個臨界值,參數repository用于設置臨時文件的存儲目錄。DefaultFileItemFactory類中也定義了用于設置和讀取sizeThreshold和repository屬性的setter和getter方法,對于通過第一個構造方法創建的DefaultFileItemFactory實例對象,可以通過相應的setter方法設置sizeThreshold和repository屬性。
2. createItem方法
DefaultFileItemFactory類的createItem方法用于創建一個FileItem對象,其源代碼如下:
public FileItem createItem(String fieldName,
String contentType,boolean isFormField,String fileName)
{
return new DefaultFileItem(fieldName, contentType,
isFormField, fileName, sizeThreshold, repository);
}
createItem方法中的各個參數的詳細信息請參看1.2.5節中的DefaultFileItem類的構造方法的講解。
1.3.7 DiskFileUpload類
DiskFileUpload類繼承了FileUploadBase類,它是Apache文件上傳組件對外提供的入口操作類,應用程序開發人員通過這個類來與Apache文件上傳組件進行交互。DiskFileUpload類除了定義了各個成員變量外,其中還定義了許多代表各個頭字段名稱和參數名稱的靜態常量。
1.構造方法
DiskFileUpload類有兩個重載的構造方法,它們的源代碼如下:
public DiskFileUpload()
{
super();
this.fileItemFactory = new DefaultFileItemFactory();
}
public DiskFileUpload(DefaultFileItemFactory fileItemFactory)
{
super();
this.fileItemFactory = fileItemFactory;
}
可見,DiskFileUpload類的構造方法的主要作用是準備FileItemFactory類的實例對象,以便以后調用這個FileItemFactory實例對象來創建各個FileItem對象。
2.isMultipartContent方法
DiskFileUpload類的isMultipartContent方法是從父類繼承來的,其作用是判斷當前請求消息的類型是不是“
multipart/form-data
”,如果是則返回true,否則返回false。isMultipartContent方法的源代碼如下:public static final boolean isMultipartContent(HttpServletRequest req)
{
//靜態常量CONTENT_TYPE ="
Content-type
" String contentType = req.getHeader(CONTENT_TYPE);
if (contentType == null)
{
return false;
}
//靜態常量MULTIPART
=
"multipart/
" if (contentType.startsWith(MULTIPART))
{
return true;
}
return false;
}
3. parseHeaders方法
DiskFileUpload類的parseHeaders方法是從父類繼承來的,其作用是從MulitpartStream類解析出的一個分區的描述頭串字符中提取各個頭字段的信息,并將這些信息保存到一個HashMap對象中后返回,HashMap中的關鍵字為頭字段的名稱,HashMap中的值為字段的值。參看圖1.3,可以知道通常有兩個字段名:Content-Disposition和Content-Type,parseHeaders方法會將它們統一轉換成小寫的形式后再保存到HashMap對象中。parseHeaders方法的源代碼如下,參數headerPart就是從MulitpartStream類的readHeaders方法返回的一個分區的描述頭字符串:
protected Map parseHeaders(String headerPart)
{
Map headers = new HashMap();
// int MAX_HEADER_SIZE = 1024
char buffer[] = new char[MAX_HEADER_SIZE];
boolean done = false;
int j = 0;
int i;
String header, headerName, headerValue;
try
{
while (!done)
{
i = 0;
//從描述頭部分讀取一行信息,行結束的依據是連續的回車換行符
while (i < 2 || buffer[i - 2] != '\r' || buffer[i - 1] != '\n')
{
buffer[i++] = headerPart.charAt(j++);
}
// 丟掉最后的回車換行符
header = new String(buffer, 0, i - 2);
//如果為空行,表示處理完了整個描述頭字符串,結束循環
if (header.equals(""))
{
done = true;
}
else
{
if (header.indexOf(':') == -1)
{
//這一行格式有問題,跳過這行后繼續下一行的處理
continue;
}
//獲取頭字段的名稱,并將它們統一成小寫形式
headerName = header.substring(0, header.indexOf(':'))
.trim().toLowerCase();
headerValue =
header.substring(header.indexOf(':') + 1).trim();
/*將頭字段信息保存到HashMap對象中,如果一個頭字段出現了多次,
對每次的值進行組合,并以逗號進行分隔
*/
if (getHeader(headers, headerName) != null)
{
headers.put(headerName,
getHeader(headers, headerName) + ','+ headerValue);
}
else
{
headers.put(headerName, headerValue);
}
}
}
}
catch (IndexOutOfBoundsException e)
{
// 如果有異常,說明描述部分格式有問題,不作任何處理
}
return headers;
}
4. getHeader方法
DiskFileUpload類的getHeader方法是從父類繼承來的,它的作用是從保存各個頭字段信息的HashMap 對象中檢索某個頭字段的值。getHeader方法的源代碼如下,其中參數headers代表parseHeaders方法創建的HashMap對象,參數name代表字段的名稱:
protected final String getHeader(Map headers, String name)
{
return (String) headers.get(name.toLowerCase());
}
5. getFieldName方法
DiskFileUpload類的getFieldName方法是從父類繼承來的,該方法從描述頭部分的content-disposition頭字段中檢索name參數的值,也就是獲取一個分區所對應的表單字段的名稱。getFieldName方法的源代碼如下:
protected String getFieldName(Map headers)
{
String fieldName = null;
// 靜態常量CONTENT_DISPOSITION =“content-disposition”
String cd = getHeader(headers, CONTENT_DISPOSITION);
// 靜態常量FORM_DATA = "form-data"
if (cd != null && cd.startsWith(FORM_DATA))
{
int start = cd.indexOf("name=\" ");
int end = cd.indexOf(‘"‘, start + 6);
if (start != -1 && end != -1)
{
fieldName = cd.substring(start + 6, end);
}
}
return fieldName;
}
6. getFileName方法
DiskFileUpload類的getFileName方法是從父類繼承來的,該方法從描述頭部分的content-disposition頭字段中檢索filename參數的值,也就是獲得文件上傳字段中的文件名。getFileName方法的源代碼如下:
protected String getFileName(Map /* String, String */ headers)
{
String fileName = null;
String cd = getHeader(headers, CONTENT_DISPOSITION);
//靜態常量ATTACHMENT = "attachment"
if (cd.startsWith(FORM_DATA) || cd.startsWith(ATTACHMENT))
{
int start = cd.indexOf("filename=\"");
int end = cd.indexOf('"', start + 10);
if (start != -1 && end != -1)
{
fileName = cd.substring(start + 10, end).trim();
}
}
return fileName;
}
7. createItem方法
DiskFileUpload類的createItem方法是從父類繼承來的,它調用作為成員變量的DefaultFileItemFactory實例對象來創建一個DefaultFileItem類對象。createItem方法創建出的DefaultFileItem類對象當前只包含從分區的描述頭部分提取出的信息,而沒有包含分區的主體部分的內容。createItem方法的源代碼如下:
protected FileItem createItem(Map headers,
boolean isFormField) throws FileUploadException
{
return getFileItemFactory().createItem(getFieldName(headers),
getHeader(headers, CONTENT_TYPE),isFormField, getFileName(headers));
}
8. parseRequest方法
DiskFileUpload類的parseRequest方法是從父類繼承來的,它是DiskFileUpload類的核心方法。parseRequest方法負責創建MultipartStream類的實例對象,并調用該對象來解析請求消息中實體內容,然后再調用作為成員變量的DefaultFileItemFactory對象將解析出來數據封裝成一個個的DefaultFileItem對象,最后將這些FileItem對象加入進一個List類型的集合對象中返回。parseRequest方法的源代碼如下:
public List parseRequest(HttpServletRequest req)
throws FileUploadException
{
if (null == req)
{
throw new NullPointerException("req parameter");
}
//定義用于存儲所有FileItem對象的List集合
ArrayList items = new ArrayList();
String contentType = req.getHeader(CONTENT_TYPE);
if ((null == contentType) || (!contentType.startsWith(MULTIPART)))
{
throw new InvalidContentTypeException("the request doesn't contain a "
+ MULTIPART_FORM_DATA // 靜態成員常量"multipart/form-data"
+ " or "
+ MULTIPART_MIXED // 靜態成員常量"multipart/mixed"
+ " stream, content type header is " + contentType);
}
// 獲得請求消息的實體內容的大小
int requestSize = req.getContentLength();
if (requestSize == -1)
{
throw new UnknownSizeException(
"the request was rejected because it's size is unknown");
}
// 實體內容超過了用戶限定的大小
if (sizeMax >= 0 && requestSize > sizeMax)
{
throw new SizeLimitExceededException(
"the request was rejected because "
+ "it's size exceeds allowed range");
}
try
{
//從請求消息的Content-Type頭字段中獲取分隔界線
int boundaryIndex = contentType.indexOf("boundary=");
if (boundaryIndex < 0)
{
throw new FileUploadException(
"the request was rejected because "
+ "no multipart boundary was found");
}
byte[] boundary = contentType.substring(
boundaryIndex + 9).getBytes();
//獲取實體內容
InputStream input = req.getInputStream();
//創建MultipartStream對象并對實體內容進行解析
MultipartStream multi = new MultipartStream(input, boundary);
multi.setHeaderEncoding(headerEncoding);
boolean nextPart = multi.skipPreamble();
while (nextPart)
{
Map headers = parseHeaders(multi.readHeaders());
String fieldName = getFieldName(headers)
if (fieldName != null)
{
String subContentType = getHeader(headers, CONTENT_TYPE);
/* 在文件上傳中很少見到"multipart/mixed"這種類型,
讀者可直接跳過下面的if語句塊,只看對應else語句塊*/
if (subContentType != null && subContentType
.startsWith(MULTIPART_MIXED))
{
// multipart/mixed的文件嵌套了幾個文件,因此解析嵌套文件
byte[] subBoundary = subContentType.substring(
subContentType.indexOf("boundary=") + 9).getBytes();
// 獲得用于分隔嵌套文件的分隔界線
multi.setBoundary(subBoundary);
boolean nextSubPart = multi.skipPreamble();
while (nextSubPart)
{
headers = parseHeaders(multi.readHeaders());
if (getFileName(headers) != null)
{
FileItem item = createItem(headers, false);
OutputStream os = item.getOutputStream();
try
{
multi.readBodyData(os);
}
finally
{
os.close();
}
items.add(item);
}
else
{
multi.discardBodyData();
}
nextSubPart = multi.readBoundary();
}
multi.setBoundary(boundary);
}
else //讀者只需關心這個else語句塊
{
if (getFileName(headers) != null)
{
// 此時處理的是一個文件表單字段
FileItem item = createItem(headers, false);
OutputStream os = item.getOutputStream();
try
{
multi.readBodyData(os);
}
finally
{
os.close();
}
items.add(item);
}
else
{
//此時處理的是一個普通表單字段
FileItem item = createItem(headers, true);
OutputStream os = item.getOutputStream();
try
{
multi.readBodyData(os);
}
finally
{
os.close();
}
items.add(item);
}
}
}
else //表單字段的名稱不存在,丟棄該分區的內容
{
multi.discardBodyData();
}
nextPart = multi.readBoundary();//讀取下一個分隔界線
}
}
catch (IOException e)
{
throw new FileUploadException(
"Processing of " + MULTIPART_FORM_DATA
+ " request failed. " + e.getMessage());
}
return items;
}
在DiskFileUpload類中還定義了parseRequest方法的一個重載形式,
這個重載的parseRequest方法內部調用了上面的parseRequest方法,同時設置了一些參數信息。這個重載的parseRequest方法的源代碼如下:
public List parseRequest(HttpServletRequest req,
int sizeThreshold,long sizeMax, String path) throws FileUploadException
{
setSizeThreshold(sizeThreshold);//設置要切換存儲模式的臨界值
setSizeMax(sizeMax); // 設置允許上傳的文件大小
setRepositoryPath(path); // 臨時文件存儲路徑
return parseRequest(req);
}
本節分析了Apache文件上傳組件的實現思想和主要的源代碼,建議讀者參照本節的方式多分析一些開源項目。