setSizeMax方法用于設置請求消息實(shí)體內容的最大允許大小,以防止客戶(hù)端故意通過(guò)上傳特大的文件來(lái)塞滿(mǎn)服務(wù)器端的存儲空間,單位為字節。
2.setSizeThreshold方法
Apache文件上傳組件在解析和處理上傳數據中的每個(gè)字段內容時(shí),需要臨時(shí)保存解析出的數據。因為Java虛擬機默認可以使用的內存空間是有
限的(筆者測試不大于100M),超出限制時(shí)將會(huì )發(fā)生“java.lang.OutOfMemoryError”錯誤,如果上傳的文件很大,例如上傳800M的文件,在
內存中將無(wú)法保存該文件內容,Apache文件上傳組件將用臨時(shí)文件來(lái)保存這些數據;但如果上傳的文件很小,例如上傳600個(gè)字節的文件,顯然
將其直接保存在內存中更加有效。setSizeThreshold方法用于設置是否使用臨時(shí)文件保存解析出的數據的那個(gè)臨界值,該方法傳入的參數的單。
3. setRepositoryPath方法
setRepositoryPath方法用于設置setSizeThreshold方法中提到的臨時(shí)文件的存放目錄,這里要求使用絕對路徑。其完整語(yǔ)法定義如下:
public void setRepositoryPath(String repositoryPath)
如果不設置存放路徑,那么臨時(shí)文件將被儲存在"java.io.tmpdir"這個(gè)JVM環(huán)境屬性所指定的目錄中,tomcat 5.5.9將這個(gè)屬性設置為了
“<tomcat安裝目錄>/temp/”目錄。
4. parseRequest方法
parseRequest 方法是DiskFileUpload類(lèi)的重要方法,它是對HTTP請求消息進(jìn)行解析的入口方法,如果請求消息中的實(shí)體內容的類(lèi)型不是
“multipart/form-data”,該方法將拋出FileUploadException異常。parseRequest 方法解析出FORM表單中的每個(gè)字段的數據,并將它們分別
包裝成獨立的FileItem對象,然后將這些FileItem對象加入進(jìn)一個(gè)List類(lèi)型的集合對象中返回。parseRequest 方法的完整語(yǔ)法定義如下:
public List parseRequest(HttpServletRequest req)
parseRequest 方法還有一個(gè)重載方法,該方法集中處理上述所有方法的功能,其完整語(yǔ)法定義如下:
parseRequest(HttpServletRequest req,int sizeThreshold,long sizeMax,
String path)
這兩個(gè)parseRequest方法都會(huì )拋出FileUploadException異常。
5. isMultipartContent方法
isMultipartContent方法方法用于判斷請求消息中的內容是否是“multipart/form-data”類(lèi)型,是則返回true,否則返回false。
isMultipartContent方法是一個(gè)靜態(tài)方法,不用創(chuàng )建DiskFileUpload類(lèi)的實(shí)例對象即可被調用,其完整語(yǔ)法定義如下:
public static final boolean isMultipartContent(HttpServletRequest req)
6. setHeaderEncoding方法
由于瀏覽器在提交FORM表單時(shí),會(huì )將普通表單中填寫(xiě)的文本內容傳遞給服務(wù)器,對于文件上傳字段,除了傳遞原始的文件內容外,還要傳遞其
文件路徑名等信息,如后面的圖1.3所示。不管FORM表單采用的是“application/x-www-form-urlencoded”編碼,還是“multipart/form-data
”編碼,它們僅僅是將各個(gè)FORM表單字段元素內容組織到一起的一種格式,而這些內容又是由某種字符集編碼來(lái)表示的。關(guān)于瀏覽器采用何種
字符集來(lái)編碼FORM表單字段中的內容,請參看筆者編著(zhù)的《深入體驗java Web開(kāi)發(fā)內幕——核心基礎》一書(shū)中的第6.9.2的講解,
“multipart/form-data”類(lèi)型的表單為表單字段內容選擇字符集編碼的原理和方式與“application/x-www-form-urlencoded”類(lèi)型的表單是
相同的。FORM表單中填寫(xiě)的文本內容和文件上傳字段中的文件路徑名在內存中就是它們的某種字符集編碼的字節數組形式,Apache文件上傳組
件在讀取這些內容時(shí),必須知道它們所采用的字符集編碼,才能將它們轉換成正確的字符文本返回。
對于瀏覽器上傳給WEB服務(wù)器的各個(gè)表單字段的描述頭內容,Apache文件上傳組件都需要將它們轉換成字符串形式返回,setHeaderEncoding 方
法用于設置轉換時(shí)所使用的字符集編碼,其原理與筆者編著(zhù)的《深入體驗java Web開(kāi)發(fā)內幕——核心基礎》一書(shū)中的第6.9.4節講解的
ServletRequest.setCharacterEncoding方法相同。setHeaderEncoding 方法的完整語(yǔ)法定義如下:
public void setHeaderEncoding(String encoding)
其中,encoding參數用于指定將各個(gè)表單字段的描述頭內容轉換成字符串時(shí)所使用的字符集編碼。
注意:如果讀者在使用Apache文件上傳組件時(shí)遇到了中文字符的亂碼問(wèn)題,一般都是沒(méi)有正確調用setHeaderEncoding方法的原因。
1.2.3 FileItem類(lèi)
FileItem類(lèi)用來(lái)封裝單個(gè)表單字段元素的數據,一個(gè)表單字段元素對應一個(gè)FileItem對象,通過(guò)調用FileItem對象的方法可以獲得相關(guān)表單字
段元素的數據。FileItem是一個(gè)接口,在應用程序中使用的實(shí)際上是該接口一個(gè)實(shí)現類(lèi),該實(shí)現類(lèi)的名稱(chēng)并不重要,程序可以采用FileItem接
口類(lèi)型來(lái)對它進(jìn)行引用和訪(fǎng)問(wèn),為了便于講解,這里將FileItem實(shí)現類(lèi)稱(chēng)之為FileItem類(lèi)。FileItem類(lèi)還實(shí)現了Serializable接口,以支持序
列化操作。
對于“multipart/form-data”類(lèi)型的FORM表單,瀏覽器上傳的實(shí)體內容中的每個(gè)表單字段元素的數據之間用字段分隔界線(xiàn)進(jìn)行分割,兩個(gè)分隔
界線(xiàn)間的內容稱(chēng)為一個(gè)分區,每個(gè)分區中的內容可以被看作兩部分,一部分是對表單字段元素進(jìn)行描述的描述頭,另外一部是表單字段元素的
主體內容,如圖1.3所示。
圖 1.3
主體部分有兩種可能性,要么是用戶(hù)填寫(xiě)的表單內容,要么是文件內容。FileItem類(lèi)對象實(shí)際上就是對圖1.3中的一個(gè)分區的數據進(jìn)行封裝的對
象,它內部用了兩個(gè)成員變量來(lái)分別存儲描述頭和主體內容,其中保存主體內容的變量是一個(gè)輸出流類(lèi)型的對象。當主體內容的大小小于
DiskFileUpload.setSizeThreshold方法設置的臨界值大小時(shí),這個(gè)流對象關(guān)聯(lián)到一片內存,主體內容將會(huì )被保存在內存中。當主體內容的數據
超過(guò)DiskFileUpload.setSizeThreshold方法設置的臨界值大小時(shí),這個(gè)流對象關(guān)聯(lián)到硬盤(pán)上的一個(gè)臨時(shí)文件,主體內容將被保存到該臨時(shí)文件
中。臨時(shí)文件的存儲目錄由DiskFileUpload.setRepositoryPath方法設置,臨時(shí)文件名的格式為“upload_00000005(八位或八位以上的數字)
.tmp”這種形式,FileItem類(lèi)內部提供了維護臨時(shí)文件名中的數值不重復的機制,以保證了臨時(shí)文件名的唯一性。當應用程序將主體內容保存
到一個(gè)指定的文件中時(shí),或者在FileItem對象被垃圾回收器回收時(shí),或者Java虛擬機結束時(shí),Apache文件上傳組件都會(huì )嘗試刪除臨時(shí)文件,以
盡量保證臨時(shí)文件能被及時(shí)清除。
下面介紹FileItem類(lèi)中的幾個(gè)常用的方法:
1. isFormField方法
isFormField方法用于判斷FileItem類(lèi)對象封裝的數據是否屬于一個(gè)普通表單字段,還是屬于一個(gè)文件表單字段,如果是普通表單字段則返回
true,否則返回false。該方法的完整語(yǔ)法定義如下:
public boolean isFormField()
2. getName方法
getName方法用于獲得文件上傳字段中的文件名,對于圖1.3中的第三個(gè)分區所示的描述頭,getName方法返回的結果為字符串“C:\bg.gif”。
如果FileItem類(lèi)對象對應的是普通表單字段,getName方法將返回null。即使用戶(hù)沒(méi)有通過(guò)網(wǎng)頁(yè)表單中的文件字段傳遞任何文件,但只要設置了
文件表單字段的name屬性,瀏覽器也會(huì )將文件字段的信息傳遞給服務(wù)器,只是文件名和文件內容部分都為空,但這個(gè)表單字段仍然對應一個(gè)
FileItem對象,此時(shí),getName方法返回結果為空字符串"",讀者在調用Apache文件上傳組件時(shí)要注意考慮這個(gè)情況。getName方法的完整語(yǔ)法
定義如下:
public String getName()
注意:如果用戶(hù)使用Windows系統上傳文件,瀏覽器將傳遞該文件的完整路徑,如果用戶(hù)使用Linux或者Unix系統上傳文件,瀏覽器將只傳遞該
文件的名稱(chēng)部分。
3.getFieldName方法
getFieldName方法用于返回表單字段元素的name屬性值,也就是返回圖1.3中的各個(gè)描述頭部分中的name屬性值,例如“name=p1”中的“p1”
。getFieldName方法的完整語(yǔ)法定義如下:
public String getFieldName()
4. write方法
write方法用于將FileItem對象中保存的主體內容保存到某個(gè)指定的文件中。如果FileItem對象中的主體內容是保存在某個(gè)臨時(shí)文件中,該方法
順利完成后,臨時(shí)文件有可能會(huì )被清除。該方法也可將普通表單字段內容寫(xiě)入到一個(gè)文件中,但它主要用途是將上傳的文件內容保存在本地文
件系統中。其完整語(yǔ)法定義如下:
public void write(File file)
5.getString方法
getString方法用于將FileItem對象中保存的主體內容作為一個(gè)字符串返回,它有兩個(gè)重載的定義形式:
public java.lang.String getString()
public java.lang.String getString(java.lang.String encoding)
throws java.io.UnsupportedEncodingExcep
前者使用缺省的字符集編碼將主體內容轉換成字符串,后者使用參數指定的字符集編碼將主體內容轉換成字符串。如果在讀取普通表單字段元
素的內容時(shí)出現了中文亂碼現象,請調用第二個(gè)getString方法,并為之傳遞正確的字符集編碼名稱(chēng)。
6. getContentType方法
getContentType 方法用于獲得上傳文件的類(lèi)型,對于圖1.3中的第三個(gè)分區所示的描述頭,getContentType方法返回的結果為字符串
“image/gif”,即“Content-Type”字段的值部分。如果FileItem類(lèi)對象對應的是普通表單字段,該方法將返回null。getContentType 方法
的完整語(yǔ)法定義如下:
public String getContentType()
7. isInMemory方法
isInMemory方法用來(lái)判斷FileItem類(lèi)對象封裝的主體內容是存儲在內存中,還是存儲在臨時(shí)文件中,如果存儲在內存中則返回true,否則返回
false。其完整語(yǔ)法定義如下:
public boolean isInMemory()
8. delete方法
delete方法用來(lái)清空FileItem類(lèi)對象中存放的主體內容,如果主體內容被保存在臨時(shí)文件中,delete方法將刪除該臨時(shí)文件。盡管Apache組件
使用了多種方式來(lái)盡量及時(shí)清理臨時(shí)文件,但系統出現異常時(shí),仍有可能造成有的臨時(shí)文件被永久保存在了硬盤(pán)中。在有些情況下,可以調用
這個(gè)方法來(lái)及時(shí)刪除臨時(shí)文件。其完整語(yǔ)法定義如下:
public void delete()
1.2.4 FileUploadException類(lèi)
在文件上傳過(guò)程中,可能發(fā)生各種各樣的異常,例如網(wǎng)絡(luò )中斷、數據丟失等等。為了對不同異常進(jìn)行合適的處理,Apache文件上傳組件還開(kāi)發(fā)
了四個(gè)異常類(lèi),其中FileUploadException是其他異常類(lèi)的父類(lèi),其他幾個(gè)類(lèi)只是被間接調用的底層類(lèi),對于A(yíng)pache組件調用人員來(lái)說(shuō),只需對
FileUploadException異常類(lèi)進(jìn)行捕獲和處理即可。
1.2.5 文件上傳編程實(shí)例
下面參考圖1.2中看到的示例代碼編寫(xiě)一個(gè)使用Apache文件上傳組件來(lái)上傳文件的例子程序。
:動(dòng)手體驗:使用Apache文件上傳組件
(1)在<tomcat安裝目錄>\webapps\fileupload目錄中按例程1-1編寫(xiě)一個(gè)名為FileUpload.html的HTML頁(yè)面,該頁(yè)面用于提供文件上傳的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>測試文件上傳組件的頁(yè)面</h3>
<form action="servlet/UploadServlet"
enctype="multipart/form-data" method="post">
作者:<input type="text" name="author"><br>
來(lái)自:<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 )建一個(gè)名為UploadServlet.java的Servlet程序,UploadServlet.java
調用Apache文件上傳組件來(lái)處理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("無(wú)法訪(fǎng)問(wèn)存儲目錄!");
return;
}
File fUploadDir = new File(uploadDir);
if(!fUploadDir.exists())
{
if(!fUploadDir.mkdir())
{
out.println("無(wú)法創(chuàng )建存儲目錄!");
return;
}
}
if (!DiskFileUpload.isMultipartContent(request))
{
out.println("只能處理multipart/form-data類(lèi)型的數據!");
return ;
}
DiskFileUpload fu = new DiskFileUpload();
//最多上傳200M數據
fu.setSizeMax(1024 * 1024 * 200);
//超過(guò)1M的字段數據采用臨時(shí)文件緩存
fu.setSizeThreshold(1024 * 1024);
//采用默認的臨時(shí)文件存儲位置
//fu.setRepositoryPath(...);
//設置上傳的普通字段的名稱(chēng)和文件字段的文件名所采用的字符集編碼
fu.setHeaderEncoding("gb2312");
//得到所有表單字段對象的集合
List fileItems = null;
try
{
fileItems = fu.parseRequest(request);
}
catch (FileUploadException e)
{
out.println("解析數據時(shí)出現如下問(wèn)題:");
e.printStackTrace(out);
return;
}
//處理每個(gè)表單字段
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();
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("存儲文件時(shí)出現如下問(wèn)題:");
e.printStackTrace(out);
return;
}
finally //總是立即刪除保存表單字段內容的臨時(shí)文件
{
fi.delete();
}
}
}
//顯示處理結果
out.println("用戶(hù):" + request.getAttribute("author") + "<br>");
out.println("來(lái)自:" + request.getAttribute("company") + "<br>");
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 ? "無(wú)" : filelist.toString()));
}
private void makeUpList(StringBuffer result,String fragment)
{
if(fragment != null)
{
if(result.length() != 0)
{
result.append(",");
}
result.append(fragment);
}
}
}
在Windows資源管理器窗口中將UploadServlet.java源文件拖動(dòng)到compile.bat文件的快捷方式上進(jìn)行編譯,修改Javac編譯程序報告的錯誤,直
到編譯成功通過(guò)為止。
(3)修改<tomcat的安裝目錄>\webapps\fileupload\WEB-INF\classes\web.xml文件,在其中注冊和映射UploadServlet的訪(fǎng)問(wèn)路徑,如例程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)重新啟動(dòng)Tomcat,并在瀏覽器地址欄中輸入如下地址:
http://localhost:8080/fileupload/FileUpload.html
填寫(xiě)返回頁(yè)面中的FORM表單,如圖1.4所示,單擊“上載”按鈕后,瀏覽器返回的頁(yè)面信息如圖1.5所示。
圖1.4
圖1.5(這些圖的標題欄中的it315改為fileupload)
查看<tomcat安裝目錄>\webapps\it315\upload目錄,可以看到剛才上傳的兩個(gè)文件。
(4)單擊瀏覽器工具欄上的“后退”按鈕回到表單填寫(xiě)頁(yè)面,只在第二個(gè)文件字段中選擇一個(gè)文件,單擊“上載”按鈕,瀏覽器返回的顯示結果
如圖1.6所示。
圖1.6
M腳下留心:
上面編寫(xiě)的Servlet程序將上傳的文件保存在了當前WEB應用程序下面的upload目錄中,這個(gè)目錄是客戶(hù)端瀏覽器可以訪(fǎng)問(wèn)到的目錄。如果用戶(hù)
通過(guò)瀏覽器上傳了一個(gè)名稱(chēng)為test.jsp的文件,那么用戶(hù)接著(zhù)就可以在瀏覽器中訪(fǎng)問(wèn)這個(gè)test.jsp文件了,對于本地瀏覽器來(lái)說(shuō),這個(gè)jsp文件
的訪(fǎng)問(wèn)URL地址如下所示:
http://localhost:8080/fileupload/upload/test.jsp
對于遠程客戶(hù)端瀏覽器而言,只需要將上面的url地址中的localhost改寫(xiě)為T(mén)omcat服務(wù)器的主機名或IP地址即可。用戶(hù)可以通過(guò)上面的Servlet
程序來(lái)上傳自己編寫(xiě)的jsp文件,然后又可以通過(guò)瀏覽器來(lái)訪(fǎng)問(wèn)這個(gè)jsp文件,如果用戶(hù)在jsp文件中編寫(xiě)一些有害的程序代碼,例如,查看服務(wù)
器上的所有目錄結構,調用服務(wù)器上的操作系統進(jìn)程等等,這將是一個(gè)非常致命的安全漏洞和隱患,這臺服務(wù)器對外就沒(méi)有任何安全性可言了
。
1.3 Apache文件上傳組件的源碼賞析
經(jīng)常閱讀一些知名的開(kāi)源項目的源代碼,可以幫助我們開(kāi)闊眼界和快速提高編程能力。Apache文件上傳組件是Apache組織開(kāi)發(fā)的一個(gè)開(kāi)源項目
,從網(wǎng)址http://jakarta.apache.org/commons/fileupload可以下載到Apache組件的源程序包,在本書(shū)的附帶帶光盤(pán)中也提供了該組件的源程
序包,文件名為commons-fileupload-1.0-src.zip。該組件的設計思想和程序編碼細節包含有許多值得借鑒的技巧,為了便于有興趣的讀者學(xué)
習和研究該組件的源碼,本節將分析Apache文件上傳組件的源代碼實(shí)現。對于只想了解如何使用Apache文件上傳組件來(lái)上傳文件的讀者來(lái)說(shuō),
不必學(xué)習本節的內容。在學(xué)習本節內容之前,讀者需要仔細學(xué)習了筆者編著(zhù)的《深入體驗java Web開(kāi)發(fā)內幕——核心基礎》一書(shū)中的第6.7.2節
中講解的“分析文件上傳的請求消息結構”的知識。
1.3.1 Apache文件上傳組的類(lèi)工作關(guān)系
Apache文件上傳組件總共由兩個(gè)接口,十二個(gè)類(lèi)組成。在A(yíng)pache文件上傳組件的十二個(gè)類(lèi)中,有兩個(gè)抽象類(lèi),四個(gè)的異常類(lèi),六個(gè)主要類(lèi),其
中FileUpLoad類(lèi)用暫時(shí)沒(méi)有應用,是為了以后擴展而保留的。Apache文件上傳組件中的各個(gè)類(lèi)的關(guān)系如圖1.7所示,圖中省略了異常類(lèi)。
圖 1.7
DiskFileUpload類(lèi)是文件上傳組件的核心類(lèi),它是一個(gè)總的控制類(lèi),首先由Apache文件上傳組件的使用者直接調用DiskFileUpload類(lèi)的方法,
DiskFileUpload類(lèi)再調用和協(xié)調更底層的類(lèi)來(lái)完成具體的功能。解析類(lèi)MultipartStream和工廠(chǎng)類(lèi)DefaultFileItemFactory就是DiskFileUpload
類(lèi)調用的兩個(gè)的底層類(lèi)。MultipartStream類(lèi)用于對請求消息中的實(shí)體數據進(jìn)行具體解析,DefaultFileItemFactory類(lèi)對MultipartStream類(lèi)解
析出來(lái)的數據進(jìn)行封裝,它將每個(gè)表單字段數據封裝成一個(gè)個(gè)的FileItem類(lèi)對象,用戶(hù)通過(guò)FileItem類(lèi)對象來(lái)獲得相關(guān)表單字段的數據。
DefaultFileItem是FileItem接口的實(shí)現類(lèi),實(shí)現了FileItem接口中定義的功能,用戶(hù)只需關(guān)心FileItem接口,通過(guò)FileItem接口來(lái)使用
DefaultFileItem類(lèi)實(shí)現的功能。DefaultFileItem類(lèi)使用了兩個(gè)成員變量來(lái)分別存儲表單字段數據的描述頭和主體內容,其中保存主體內容的
變量類(lèi)型為DeferredFileOutputStream
用一個(gè)ByteArrayOutputStream類(lèi)對象來(lái)存儲數據,當寫(xiě)入它里面的主體內容的大小大于DiskFileUpload.setSizeThreshold方法設置的臨界值
時(shí),DeferredFileOutputStream
件輸出流對象中。這個(gè)文件輸出流對象關(guān)聯(lián)的文件是一個(gè)臨時(shí)文件,它的保存路徑由DiskFileUpload.setRepositoryPath方法指定。
Apache文件上傳組件的處理流程如圖1.8所示。
圖1.8
圖1.8中的每一步驟的詳細解釋如下:
(1)Web容器接收用戶(hù)的HTTP請求消息,創(chuàng )建request請求對象。
(2)調用DiskFileUpload類(lèi)對象的parseRequest方法對request請求對象進(jìn)行解析。該方法首先檢查request請求對象中的數據內容是否是
“multipart/form-data”類(lèi)型,如果是,該方法則創(chuàng )建MultipartStream類(lèi)對象對request請求對象中的請求體進(jìn)行解析。
(3)MultipartStream類(lèi)對象對request請求體進(jìn)行解析,并返回解析出的各個(gè)表單字段元素對應的內容。
(4)DiskFileUpload類(lèi)對象的parseRequest方法接著(zhù)創(chuàng )建DefaultFileItemFactory類(lèi)對象,用來(lái)將MultipartStream類(lèi)對象解析出的每個(gè)表單
字段元素的數據封裝成FileItem類(lèi)對象。
(5)DefaultFileItemFactory工廠(chǎng)類(lèi)對象把MultipartStream類(lèi)對象解析出的各個(gè)表單字段元素的數據封裝成若干DefaultFileItem類(lèi)對象,然
后加入到一個(gè)List類(lèi)型的集合對象中,parseRequest方法返回該List集合對象。
實(shí)際上,步驟(3)和步驟(5)是交替同步進(jìn)行的,即在MultipartStream類(lèi)對象解析每個(gè)表單字段元素時(shí),都會(huì )調用DefaultFileItemFactory
工廠(chǎng)類(lèi)把該表單字段元素封裝成對應的FileItem類(lèi)對象。
1.3.2 Apache文件上傳組件的核心編程問(wèn)題
WEB服務(wù)器端程序接收到“multipart/form-data”類(lèi)型的HTTP請求消息后,其核心和基本的編程工作就是讀取請求消息中的實(shí)體內容,然后解
析出每個(gè)分區的數據,接著(zhù)再從每個(gè)分區中解析出描述頭和主體內容部分。
在讀取HTTP請求消息中的實(shí)體內容時(shí),只能調用HttpServletRequest.getInputStream方法返回的字節輸入流,而不能調用
HttpServletRequest.getReader方法返回的字符輸入流,因為不管上傳的文件類(lèi)型是文本的、還是其他各種格式的二進(jìn)制內容,WEB服務(wù)器程序
要做的工作就是將屬于文件內容的那部分數據原封不動(dòng)地提取出來(lái),然后原封不動(dòng)地存儲到本地文件系統中。如果使用
HttpServletRequest.getReader方法返回的字符輸入流對象來(lái)讀取HTTP請求消息中的實(shí)體內容,它將HTTP請求消息中的字節數據轉換成字符后
再返回,這主要是為了方便要以文本方式來(lái)處理本來(lái)就全是文本內容的請求消息的應用,但本程序要求的是“原封不動(dòng)”,顯然不能使用
HttpServletRequest.getReader方法返回的字符輸入流對象來(lái)進(jìn)行讀取。
另外,不能期望用一個(gè)很大的字節數組就可以裝進(jìn)HTTP請求消息中的所有實(shí)體內容,因為程序中定義的字節數組大小總是有限制的,但應該允
許客戶(hù)端上傳超過(guò)這個(gè)字節數組大小的實(shí)體內容。所以,只能創(chuàng )建一個(gè)一般大小的字節數組緩沖區來(lái)逐段讀取請求消息中的實(shí)體內容,讀取一
段就處理一段,處理完上一段以后,再讀取下一段,如此循環(huán),直到處理完所有的實(shí)體內容,如圖1.9所示。
圖 1.9
在圖1.9中,buffer即為用來(lái)逐段讀取請求消息中的實(shí)體內容的字節數組緩沖區。因為讀取到緩沖區中的數據處理完后就會(huì )被拋棄,確切地說(shuō),
是被下一段數據覆蓋,所以,解析和封裝過(guò)程必須同步進(jìn)行,程序一旦識別出圖1.3中的一個(gè)分區的開(kāi)始后,就要開(kāi)始將它封裝到一個(gè)FileItem
對象中。
程序要識別出圖1.3中的每一個(gè)分區,需要在圖1.9所示的字節數組緩沖區buffer中尋找分區的字段分隔界線(xiàn),當找到一個(gè)字段分隔界線(xiàn)后,就
等于找到了一個(gè)分區的開(kāi)始。筆者在《深入體驗java Web開(kāi)發(fā)內幕——核心基礎》一書(shū)中的第6.7.2節中已經(jīng)講過(guò),上傳文件的請求消息的
Content-Type頭字段中包含有用作字段分隔界線(xiàn)的字符序列,如下所示:
content-type : multipart/form-data; boundary=---------------------------7d51383203e8
顯然,我們可以通過(guò)調用HttpServletRequest.getHeader方法讀取Content-Type頭字段的內容,從中分離出分隔界線(xiàn)的字符序列,然后在字節
數組緩沖區buffer中尋找分區的字段分隔界線(xiàn)。content-type頭字段的boundary參數中指定的字段分隔界線(xiàn)是瀏覽器隨機產(chǎn)生的,瀏覽器保證
它不會(huì )與用戶(hù)上傳的所有數據中的任何部分出現相同。在這里有一點(diǎn)需要注意,圖1.3中的實(shí)體內容內部的字段分隔界線(xiàn)與content-type頭中指
定的字段分隔界線(xiàn)有一點(diǎn)細微的差別,前者是在后者前面增加了兩個(gè)減號(-)字符而形成的,這倒不是什么編程難點(diǎn)。真正的編程難點(diǎn)在于在
字節數組緩沖區buffer中尋找分隔界線(xiàn)時(shí),可能會(huì )遇到字節數組緩沖區buffer中只裝入了分隔界線(xiàn)字符序列的部分內容的情況,如圖1.10所示
。
圖1.10
要解決這個(gè)問(wèn)題的方法之一就是在查找字段分隔界線(xiàn)時(shí),如果發(fā)現字節數組緩沖區buffer中只裝入了分隔界線(xiàn)字符序列的部分內容,那么就將
這一部分內容留給字節數組緩沖區buffer的下一次讀取,如圖1.11所示。
圖1.11
這種方式讓字節數組緩沖區buffer下一次讀取的內容不是緊接著(zhù)上一次讀取內容的后面,而是重疊上一次讀取的一部分內容,即從上一次讀取
聯(lián)系客服