Java Web開發人員可以使用Apache文件上傳組件來接收瀏覽器上傳的文件,該組件由多個類共同組成,但是,對於使用該組件來編寫文件上傳功能的Java Web開發人員來說,只需要瞭解和使用其中的三個類:DiskFileUpload、FileItem和FileUploadException。這三個 類全部位於org.apache.commons.fileupload包中。
1. DiskFileUpload類
DiskFileUpload類是Apache文件上傳組件的核心類,應用程序開發人員通過這個類來與Apache文件上傳組件進行交互。但現在Apache建議使用ServletFileUpload類 ,兩個類的方法類似。下面介紹DiskFileUpload類中的幾個常用的重要方法。
1.1.setSizeMax方法
setSizeMax方法用於設置請求消息實體內容的最大允許大小,以防止客戶端故意通過上傳特大的文件來塞滿服務器端的存儲空間,單位爲字節。其完整語法定義如下:
public void setSizeMax(long sizeMax)
如果請求消息中的實體內容的大小超過了setSizeMax方法的設置值,該方法將會拋出FileUploadException異常。
1.2.setSizeThreshold方法
Apache文件上傳組件在解析和處理上傳數據中的每個字段內容時,需要臨時保存解析出的數據。因爲Java虛擬機默認可以使用的內存空間是有限的 (筆者測試不大於100M),超出限制時將會發生“java.lang.OutOfMemoryError”錯誤,如果上傳的文件很大,例如上傳800M 的文件,在內存中將無法保存該文件內容,Apache文件上傳組件將用臨時文件來保存這些數據;但如果上傳的文件很小,例如上傳600個字節的文件,顯然 將其直接保存在內存中更加有效。setSizeThreshold方法用於設置是否使用臨時文件保存解析出的數據的那個臨界值,該方法傳入的參數的單位是 字節。其完整語法定義如下:
public void setSizeThreshold(int sizeThreshold)
1.3. setRepositoryPath方法
setRepositoryPath方法用於設置setSizeThreshold方法中提到的臨時文件的存放目錄,這裏要求使用絕對路徑。其完整語法定義如下:
public void setRepositoryPath(String repositoryPath)
如果不設置存放路徑,那麼臨時文件將被儲存在"java.io.tmpdir"這個JVM環境屬性所指定的目錄中,tomcat 5.5.9將這個屬性設置爲了“<tomcat安裝目錄>/temp/”目錄。
1.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異常。
1.5. isMultipartContent方法
isMultipartContent方法方法用於判斷請求消息中的內容是否是“multipart/form-data”類型,是則返回 true,否則返回false。isMultipartContent方法是一個靜態方法,不用創建DiskFileUpload類的實例對象即可被調 用,其完整語法定義如下:
public static final boolean isMultipartContent(HttpServletRequest req)
1.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方法的原因。
2. 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類中的幾個常用的方法:
2.1. isFormField方法
isFormField方法用於判斷FileItem類對象封裝的數據是否屬於一個普通表單字段,還是屬於一個文件表單字段,如果是普通表單字段則返回true,否則返回false。該方法的完整語法定義如下:
public boolean isFormField()
2.2. getName方法
getName方法用於獲得文件上傳字段中的文件名,對於圖1.3中的第三個分區所示的描述頭,getName方法返回的結果爲字符串 “C:/bg.gif”。如果FileItem類對象對應的是普通表單字段,getName方法將返回null。即使用戶沒有通過網頁表單中的文件字段傳 遞任何文件,但只要設置了文件表單字段的name屬性,瀏覽器也會將文件字段的信息傳遞給服務器,只是文件名和文件內容部分都爲空,但這個表單字段仍然對 應一個FileItem對象,此時,getName方法返回結果爲空字符串"",讀者在調用Apache文件上傳組件時要注意考慮這個情況。 getName方法的完整語法定義如下:
public String getName()
注意:如果用戶使用Windows系統上傳文件,瀏覽器將傳遞該文件的完整路徑,如果用戶使用Linux或者Unix系統上傳文件,瀏覽器將只傳遞該文件的名稱部分。
2.3.getFieldName方法
getFieldName方法用於返回表單字段元素的name屬性值,也就是返回圖1.3中的各個描述頭部分中的name屬性值,例如“name=p1”中的“p1”。getFieldName方法的完整語法定義如下:
public String getFieldName()
2.4. write方法
write方法用於將FileItem對象中保存的主體內容保存到某個指定的文件中。 如果FileItem對象中的主體內容是保存在某個臨時文件中,該方法順利完成後,臨時文件有可能會被清除。該方法也可將普通表單字段內容寫入到一個文件中,但它主要用途是將上傳的文件內容保存在本地文件系統中。其完整語法定義如下:
public void write(File file)
2.5.getString方法
getString方法用於將FileItem對象中保存的主體內容作爲一個字符串返回,它有兩個重載的定義形式:
public java.lang.String getString()
public java.lang.String getString(java.lang.String encoding)
throws java.io.UnsupportedEncodingException
前者使用缺省的字符集編碼將主體內容轉換成字符串,後者使用參數指定的字符集編碼將主體內容轉換成字符串。如果在讀取普通表單字段元素的內容時出現了中文亂碼現象,請調用第二個getString方法,併爲之傳遞正確的字符集編碼名稱。
2.6. getContentType方法
getContentType 方法用於獲得上傳文件的類型,對於圖1.3中的第三個分區所示的描述頭,getContentType方法返回的結果爲字符串“image/gif”,即 “Content-Type”字段的值部分。如果FileItem類對象對應的是普通表單字段,該方法將返回null。getContentType 方法的完整語法定義如下:
public String getContentType()
2.7. isInMemory方法
isInMemory方法用來判斷FileItem類對象封裝的主體內容是存儲在內存中,還是存儲在臨時文件中,如果存儲在內存中則返回true,否則返回false。其完整語法定義如下:
public boolean isInMemory()
2.8. delete方法
delete方法用來清空FileItem類對象中存放的主體內容,如果主體內容被保存在臨時文件中,delete方法將刪除該臨時文件。儘管 Apache組件使用了多種方式來儘量及時清理臨時文件,但系統出現異常時,仍有可能造成有的臨時文件被永久保存在了硬盤中。在有些情況下,可以調用這個 方法來及時刪除臨時文件。其完整語法定義如下:
public void delete()
3. FileUploadException類
在文件上傳過程中,可能發生各種各樣的異常,例如網絡中斷、數據丟失等等。爲了對不同異常進行合適的處理,Apache文件上傳組件還開發了四個異 常類,其中FileUploadException是其他異常類的父類,其他幾個類只是被間接調用的底層類,對於Apache組件調用人員來說,只需對FileUploadException異常類進行捕獲和處理即可 。
4. ServletRequestContext
ServletRequestContext類提供訪問request的方法。實現RequestContext接口。
核心API-DiskFileItemFactory:
DiskFileItemFactory 是創建FileItem對象的工廠,這個工廠常用方法:
1. public DiskFileItemFactory(int sizeThreshold, java.io.File repository) ,常用的構造函數。
2. public void setSizeThreshold(int sizeThreshold) ,設置內存緩衝區的大小,默認值爲10K。當上傳文件大於緩衝區大小時, fileupload組件將使用臨時文件緩存上傳文件。
3. public void setRepository(java.io.File repository) ,指定臨時文件目錄,默認值爲System.getProperty("java.io.tmpdir")。
核心API-ServletFileupLoad:
ServletFileUpload 負責處理上傳的文件數據,並將表單中每個輸入項封裝到一個FileItem對象中。常用方法有:
1. boolean isMultipartContent(HttpServletRequest request) ,判斷上傳表單是否爲上傳表單類型。
2. List parseRequest(HttpServletRequest request) ,解析request對象,並把表單中的每一個輸入項包裝到一個fileItem 對象中,並返回一個保存了所有FileItem的list集合。
3. setFileSizeMax(long fileSizeMax) ,設置上傳文件的最大尺寸值。
4. setSizeMax(long sizeMax) ,設置上傳文件總量的最大值。
5. setHeaderEncoding(java.lang.String encoding) ,設置編碼格式。如果文件路徑中存在中文可能會造成文件路徑亂碼,用此方法處理可以解決。
6. setProgressListener(ProgressListener pListener) ,設置進程監聽器,與AWT和Swing的事件處理機制一樣。文件上傳一點就會觸發ProgressListener,這樣我們就可以獲取文件上傳的進度。
上傳文件案例:
public class FileuploadServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 創建文件處理工廠,它用於生成 FileItem 對象。 DiskFileItemFactory difactory = new DiskFileItemFactory(); // 設置緩存大小,如果上傳文件超過緩存大小,將使用臨時目錄做爲緩存。 difactory.setSizeThreshold(1024 * 1024); // 設置處理工廠緩存的臨時目錄,此目錄下的文件需要手動刪除。 String dir = this .getServletContext().getRealPath( "/" ); File filedir = new File(dir + "filetemp" ); if (!filedir.exists()) filedir.mkdir(); difactory.setRepository(filedir); // 設置文件實際保存的目錄 String userdir = dir + "files" ; File fudir = new File(userdir); if (!fudir.exists()) fudir.mkdir(); // 創建 request 的解析器,它會將數據封裝到 FileItem 對象中。 ServletFileUpload sfu = new ServletFileUpload(difactory); // 解析保存在 request 中的數據並返回 list 集合 List list = null ; try { list = sfu.parseRequest(request); } catch (FileUploadException e) { e.printStackTrace(); } // 遍歷 list 集合,取出每一個輸入項的 FileItem 對象,並分別獲取數據 for (Iterator it = list.iterator(); it.hasNext();) { FileItem fi = (FileItem) it.next(); if (fi.isFormField()) { System. out .println(fi.getFieldName()); System. out .println(fi.getString()); } else { // 由於客戶端向服務器發送的文件是客戶端的全路徑,在這我們只需要文件名即可 String filename = fi.getName(); int index = filename.lastIndexOf( """" ); if (index != -1) filename = filename.substring(index+1); // 向服務器寫出文件 InputStream in = fi.getInputStream(); FileOutputStream fos = new FileOutputStream(fudir + "/" +filename); byte [] buf = new byte [1024]; int len = -1; while ((len = in.read(buf)) != -1){ fos.write(buf, 0, len); } // 關閉流 if (in != null ){ try { in.close(); } finally { if (fos!= null ) fos.close(); } } } } } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } |
上面的代碼只是功能的練習,實際開發中的文件上傳需要考慮諸多因素,我們接下來繼續學習。
JS 動態添加文件上傳框和按鈕的JavaScript代碼:
function add(){ var file = document.createElement( "input" ); file.type = "file" ; file.name = "file" ;
var butt = document.createElement( "input" ); butt.type = "button" ; butt.value = " 刪除 " ; butt.onclick = function rem(){ // 必須使用按鈕的父節點 DIV 的父節點來刪除自己和自己的父節點 DIV 。 this .parentNode.parentNode.removeChild( this .parentNode); };
var div = document.createElement( "div" ); div.appendChild(file); div.appendChild(butt);
var parent = document.getElementById( "files" ); parent.appendChild(div); } |
上傳文件的處理細節(1):
1. 中 文文件亂碼的問題,可以調用兩個方法來設置字符編碼:servletUpLoader.setHeaderEncoding()或 request.setCharacterEncoding()。我們可以在源文件的創建ServletFileUpload對象後邊添加如下代碼:
sfu.setHeaderEncoding( "UTF-8" ); |
2. 臨 時文件的刪除,如果臨時文件大於setSizeThreshold設置的緩存大小,Commons-fileupload組件將使用 setRepository設置的臨時目錄來保存上傳的文件,上傳完成後我們需要手動調用FileItem.delete來刪除臨時文件。建議不要修改緩 存區大小,如果設置緩存爲1MB,1000個用戶上傳文件就需要1000MB內存,服務器會受不了的。我們刪除掉setSizeThreshold的代 碼,並在每次完成一個文件後添加下而的代碼:
// 刪除臨時目錄中的文件 fi.delete(); |
上傳文件的處理細節(2):
1. 在上面的代碼中,我們將文件的實際保存目錄設置在WEB-INF目錄之外。這樣外部可以直接訪問被上傳的文件,這會造成安全問題。比如用戶上傳了一個帶有惡意腳本功能的JSP文件,然後從外部訪問執行了JSP文件…後果不堪設想。所以我們將源代碼中對應位置處修改如下:
// 之所以放在 "WEB-INF" 目錄下是爲了防止上傳的文件被直接被訪問的安全問題 String userdir = dir + "WEB-INF/files" ; |
2. 一個WEB應用會許多不同的用戶訪問,不同的用戶可能會上傳相同名稱的文件,如果這樣可能會造成文件覆蓋的情況發生,所以我們必須保證文件名稱的唯一性,我編寫一個方法來生成唯一性名稱的文件名:
/** * 生成具有唯一性的 UUID 文件名稱 * @param fileName * @return */ private String uuidName(String fileName){ UUID uuid = UUID.randomUUID (); return uuid.toString() + "_" + fileName; } |
我們將代碼“ filename = filename.substring(index + 1); ”修改爲: filename = uuidName(filename.substring(index + 1));
3. 如果一個目錄下的文件過多,會極大減慢文件的訪問速度。比如一個目錄下的文件如果超過1000個,達到1萬個呢?恐怖!我們必須編寫一個目錄結構生成算法,來分散存上傳的文件。我們一個方法:
/** * 使用哈希算法生成的文件路徑 * @param dir * @param fileName * @return */ private String hashPath(String dir, String fileName) { int hashCode = fileName.hashCode();
int dir1 = (hashCode >> 4) & 0xf; int dir2 = hashCode & 0xf;
String newpath = dir + "/" + dir1 + "/" + dir2 + "/" ; File file = new File(newpath); if (!file.exists()){ file.mkdirs(); }
return newpath + uuidName(fileName); } |
上傳文件的處理細節(3)
1. 使用ProgressListener顯示上傳文件進度,在創建ServletFileUpload之後添加如下代碼:
// 設置文件上傳進度監聽器 sfu.setProgressListener( new ProgressListener() { public void update( long pBytesRead, long pContentLength, int pItems) { System. out .println( " 已上傳: " + pBytesRead + " 總大小: " + pContentLength); } }); |
2. 上面的代碼會造成頻繁的打印,爲了使它在上傳一定數量後再打印,比如上傳10KB後再打印,我們修改上面的代碼如下:
// 設置文件上傳進度監聽器 sfu.setProgressListener( new ProgressListener() { long temp = -1; public void update( long pBytesRead, long pContentLength, int pItems) { long size = pBytesRead / 1024 * 1024 * 10; if ( temp == size) return ; temp = size; if (pBytesRead != -1) System. out .println( " 已上傳: " + pBytesRead + " 總大小: " + pContentLength); else System. out .println( " 上傳完成! " ); } }); |
上面的代碼比較經典,好好回味一下。
文件下載:
WEB 應用中實現文件下載的兩種方式:
1. 超鏈接直接指向下載資源
2. 程序實現下載需設置兩個響應頭:
(1). 設置Content-Type 的值爲:application/x-msdownload。Web 服務器需要告訴瀏覽器其所輸出的內容的類型不是普通的文本文件或 HTML 文件,而是一個要保存到本地的下載文件。
(2). Web 服 務器希望瀏覽器不直接處理相應的實體內容,而是由用戶選擇將相應的實體內容保存到一個文件中,這需要設置 Content-Disposition 報頭。該報頭指定了接收程序處理數據內容的方式,在 HTTP 應用中只有 attachment 是標準方式,attachment 表示要求用戶干預。在 attachment 後面還可以指定 filename 參數,該參數是服務器建議瀏覽器將實體內容保存到文件中的文件名稱。在設置 Content-Dispostion 之前一定要指定 Content-Type。
爲實現文件下載,首先我們遍歷目錄下所有文件,Servlet:
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ListFileServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 獲取目錄
String dir = this .getServletContext().getRealPath( "/WEB-INF/files" );
HashMap map = new HashMap();
listFile( new File(dir), map);
// 將文件列表設置到 request 的屬性中,然後由 JSP 頁面打印列表。
request.setAttribute( "filemap" , map);
request.getRequestDispatcher( "/list.jsp" ).forward(request, response);
}
/**
* 使用遞歸算法,將所有子目錄中的文件添加到列表中
*
* @param f
* @param l
*/
private void listFile(File f, HashMap map) {
if (f.isFile()) {
String path = f.getAbsolutePath().substring(
this .getServletContext().getRealPath( "/" ).length());
String name = f.getName();
name = name.substring(name.indexOf( "_" )+1);
//BASE64Encoder encoder = new BASE64Encoder();
map.put(path, name);
} else {
File[] files = f.listFiles();
for ( int i = 0; i < files. length ; i++) {
listFile(files[i], map);
}
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}