Apache FileUpload元件
在最初的 http 協定中,沒有上傳檔案方面的功能。RFC1867(”Form-based File Upload in HTML”.)為 http 協定添加了這個功能。用戶端的浏覽器,如 Microsoft IE, Mozila, Opera 等,按照此規範将使用者指定的檔案發送到伺服器。伺服器端的網頁程式,如 php, asp, jsp 等,可以按照此規範,解析出使用者發送來的檔案。
1.1、用戶端
簡單來說,RFC1867規範要求http協定增加了file類型的input标簽,用于浏覽需要上傳的檔案。同時要求FORM表單的enctype屬性設定為“multipart/form-data”,method屬性設定為“post”即可,下面是我們文。件上傳頁面的表單代碼:
<form action="<%=request.getContextPath()%>/Upload3Servlet"
method="post"
enctype="multipart/form-data">
File1:<input type="file" name="file"/><br/>
Desc:<input type="text" name="desc"/><br/>
<input type="submit" value="送出"/>
</form>
1.2、伺服器端
一個檔案上傳請求的消息實體由一系列根據 RFC1867(”Form-based File Upload in HTML”.)編碼的項目(文本參數和檔案參數)組成。自己程式設計來解析擷取這些資料是非常麻煩的,還需要了解RFC1867規範對請求資料編碼的相關知識。FileUpload 可以幫助我們解析這樣的請求,将每一個項目封裝成一個實作了FileItem接口的對象,并以清單的形式傳回。是以,我們隻需要了解FileUpload的API如何使用即可,不用管它們的底層實作。
讓我們來看一個簡單檔案上傳處理代碼:
package cn.itcast.servlet;
import java.io.File;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FileUtils;
/**
* 檔案上傳
*/
public class UploadServlet extends HttpServlet {
@SuppressWarnings("unchecked")
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
DiskFileItemFactory f = new DiskFileItemFactory();//磁盤對象
f.setRepository(new File("d:/a")); //設定臨時目錄
f.setSizeThreshold(1024*8); //8k的緩沖區,檔案大于8K則儲存到臨時目錄
ServletFileUpload upload = new ServletFileUpload(f);//聲明解析request的對象
upload.setHeaderEncoding("UTF-8"); //處理檔案名中文
upload.setFileSizeMax(1024 * 1024 * 5);// 設定每個檔案最大為5M
upload.setSizeMax(1024 * 1024 * 10);// 一共最多能上傳10M
String path = getServletContext().getRealPath("/imgs");//擷取檔案要儲存的目錄
try {
List<FileItem> list = upload.parseRequest(request);// 解析
for (FileItem ff : list) {
if (ff.isFormField()) {
String ds = ff.getString("UTF-8");//進行中文
System.err.println("說明是:" + ds);
} else {
String ss = ff.getName();
ss = ss.substring(ss.lastIndexOf("\\") + 1);//解析檔案名
FileUtils.copyInputStreamToFile( //直接使用commons.io.FileUtils
ff.getInputStream(),
new File(path + "/" + ss));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.3、FileItem接口
org.apache.commons.fileupload.disk.DiskFileItem實作了FileItem接口,用來封裝單個表單字段元素的資料。通過調用FileItem 定義的方法可以獲得相關表單字段元素的資料。我們不需要關心DiskFileItem的具體實作,在程式中可以采用FileItem接口類型來對DiskFileItem對象進行引用和通路。FileItem類還實作了Serializable接口,以支援序列化操作。
FileItem類常用的方法:
1. boolean isFormField()方法
isFormField方法用于判斷FileItem類對象封裝的資料是一個普通文本表單字段,還是一個檔案表單字段,如果是普通表單字段則傳回true,否則傳回false。
2. String getName()方法
getName方法用于獲得檔案上傳字段中的檔案名,即表單字段元素描述頭中的filename屬性值,如“C:\Documents and Settings\All Users\Documents\My Pictures\示例圖檔\Sunset.jpg”。如果FileItem類對象對應的是普通表單字段,getName方法将傳回null。
即使使用者沒有通過網頁表單中的檔案字段傳遞任何檔案,但隻要設定了檔案表單字段的name屬性,浏覽器也會将檔案字段的資訊傳遞給伺服器,隻是檔案名和檔案内容部分都為空,但這個表單字段仍然對應一個FileItem對象,此時,getName方法傳回結果為空字元串”“,讀者在調用Apache檔案上傳元件時要注意考慮這個情況。
注意:上面的資料包是通過IE送出,是以是完整的路徑和名稱。如
C:\Documents and Settings\All Users\Documents\My Pictures\示例圖檔\Sunset.jpg。如果是其它浏覽器,如火狐和Chromium,則僅僅是名字,沒有路徑,如Sunset.jpg。
3. String getFieldName()方法
getFieldName方法用于傳回表單字段元素描述頭的name屬性值,也是表單标簽name屬性的值。例如“name=file1”中的“file1”。
4. void write(File file)方法
write方法用于将FileItem對象中儲存的主體内容儲存到某個指定的檔案中。如果FileItem對象中的主體内容是儲存在某個臨時檔案中,該方法順利完成後,臨時檔案有可能會被清除。該方法也可将普通表單字段内容寫入到一個檔案中,但它主要用途是将上傳的檔案内容儲存在本地檔案系統中。
5. String getString()方法
getString方法用于将FileItem對象中儲存的資料流内容以一個字元串傳回,它有兩個重載的定義形式:
public java.lang.String getString()
public java.lang.String getString(java.lang.String encoding) throws java.io.UnsupportedEncodingException
前者使用預設的字元集編碼将主體内容轉換成字元串,後者使用參數指定的字元集編碼将主體内容轉換成字元串。如果在讀取普通表單字段元素的内容時出現了中文亂碼現象,請調用第二個getString方法,并為之傳遞正确的字元集編碼名稱。
6. String getContentType()方法
getContentType 方法用于獲得上傳檔案的類型,即表單字段元素描述頭屬性“Content-Type”的值,如“image/jpeg”。如果FileItem類對象對應的是普通表單字段,該方法将傳回null。
7. boolean isInMemory()方法
isInMemory方法用來判斷FileItem對象封裝的資料内容是存儲在記憶體中,還是存儲在臨時檔案中,如果存儲在記憶體中則傳回true,否則傳回false。
8. void delete()方法
delete方法用來清空FileItem類對象中存放的主體内容,如果主體内容被儲存在臨時檔案中,delete方法将删除該臨時檔案。盡管當FileItem對象被垃圾收集器收集時會自動清除臨時檔案,但及時調用delete方法可以更早的清除臨時檔案,釋放系統存儲資源。另外,當系統出現異常時,仍有可能造成有的臨時檔案被永久儲存在了硬碟中。
9. InputStream getInputStream()方法
以流的形式傳回上傳檔案的資料内容。
10. long getSize()方法
傳回該上傳檔案的大小(以位元組為機關)。
1.4、DiskFileItemFactory
此類将請求消息實體中的每一個項目封裝成單獨的DiskFileItem (FileItem接口的實作) 對象的任務由
org.apache.commons.fileupload.FileItemFactory 接口的預設實作 org.apache.commons.fileupload.disk.DiskFileItemFactory 來完成。
當上傳的檔案項目比較小時,直接儲存在記憶體中(速度比較快),比較大時,以臨時檔案的形式,儲存在磁盤臨時檔案夾(雖然速度慢些,但是記憶體資源是有限的)。
屬性:
1) public static final int DEFAULT_SIZE_THRESHOLD :
将檔案儲存在記憶體還是磁盤臨時檔案夾的預設臨界值,值為10240,即10kb。
2) private File repository:
用于配置在建立檔案項目時,當檔案項目大于臨界值時使用的臨時檔案夾,預設采用系統預設的臨時檔案路徑,可以通過系統屬java.io.tmpdir
擷取。代碼:System.getProperty(“java.io.tmpdir”);
3) private int sizeThreshold:
用于儲存将檔案儲存在記憶體還是磁盤臨時檔案夾的臨界值。
構造方法:
1) public DiskFileItemFactory():
采用預設臨界值和系統臨時檔案夾構造檔案項工廠對象。
2) public DiskFileItemFactory(int sizeThreshold,File repository):
采用參數指定臨界值和系統臨時檔案夾構造檔案項工廠對象。
其他方法:
1、FileItem createItem() 方法
根據DiskFileItemFactory相關配置将每一個請求消息實體項目建立 成DiskFileItem 執行個體,并傳回。該方法從來不需要我們親自調用,FileUpload元件在解析請求時内部使用。
2、void setSizeThreshold(int sizeThreshold)
Apache檔案上傳元件在解析上傳資料中的每個字段内容時,需要臨時儲存解析出的資料,以便在後面進行資料的進一步處理(儲存在磁盤特定位置或插入資料庫)。因為Java虛拟機預設可以使用的記憶體空間是有限的,超出限制時将會抛出“java.lang.OutOfMemoryError”錯誤。如果上傳的檔案
很大,例如800M的檔案,在記憶體中将無法臨時儲存該檔案内容,Apache檔案上傳元件轉而采用臨時檔案來儲存這些資料;但如果上傳的檔案很小,例如600個位元組的檔案,顯然将其直接儲存在記憶體中性能會更加好些。
3、setSizeThreshold
方法用于設定是否将上傳檔案已臨時檔案的形式儲存在磁盤的臨界值(以位元組為機關的int值),如果從沒有調用該方法設定此臨界值,将會采用系統預設值10KB。對應的getSizeThreshold() 方法用來擷取此臨界值。
4、void setRepository(File repository)
setRepositoryPath方法用于設定當上傳檔案尺寸大于setSizeThreshold方法設定的臨界值時,将檔案以臨時檔案形式儲存在磁盤上的存放目錄。有一個對應的獲得臨時檔案夾的 File getRespository() 方法。
注意:當從沒有調用此方法設定臨時檔案存儲目錄時,預設采用系統預設的臨時檔案路徑,可以通過系統屬性 java.io.tmpdir 擷取。
如下代碼:
System.getProperty(“java.io.tmpdir”);
Tomcat系統預設臨時目錄為
“<tomcat安裝目錄>/temp/”。
說明:
使用Listlist=servletFileUpload.parseRequest(httpServletRequest);方法,則臨時目錄受使用者設定的管理。
即,如果使用者設定的臨時目錄為d:/a,則當檔案上傳大于,大于緩沖區設定時會向d:/a下儲存臨時檔案。如果使用者沒有設定臨時目錄,才會将臨時檔案儲存到CATALINA_HOME\temp目錄下。
此種方式儲存的臨時檔案名為:upload_2eb46fea_13615ef5327__8000_00000000.tmp
使用
FileItemIterator fii=servletFileUpload.getItemIterator(httpServletRequest)方法時,則不受使用者設定臨時目錄的影響。
總是會将檔案保臨時檔案儲存到CATALINA_HOME\temp目錄下。
此種情況下儲存的臨時檔案名為:hsperfdata_Administrator (這是一個檔案夾,用裡面的檔案做為資料互動)
圖示:
1.5、ServletFileUpload類
org.apache.commons.fileupload.servlet.ServletFileUpload類是Apache檔案上傳元件處理檔案上傳的核心進階類(所謂進階就是不需要管底層實作,暴露給使用者的簡單易用的接口)。
使用其 parseRequest(HttpServletRequest) 方法可以将通過表單中每一個HTML标簽送出的資料封裝成一個FileItem對象,然後以List清單的形式傳回。使用該方法處理上傳檔案簡單易用。
如果你希望進一步提高性能,你可以采用 getItemIterator 方法,直接獲得每一個檔案項的資料輸入流,對資料做直接處理。
在使用ServletFileUpload對象解析請求時需要根據DiskFileItemFactory對象的屬性 sizeThreshold(臨界值)和repository(臨時目錄) 來決定将解析得到的資料儲存在記憶體還是臨時檔案中,如果是臨時檔案,儲存在哪個臨時目錄中?
是以,我們需要在進行解析工作前構造好DiskFileItemFactory對象,
通過ServletFileUpload對象的構造方法或setFileItemFactory()方法設定 ServletFileUpload對象的fileItemFactory屬性。
ServletFileUpload的繼承結構為:
構造方法:
1) public ServletFileUpload():
構造一個未初始化的執行個體,需要在解析請求之前先調用setFileItemFactory()方法設定 fileItemFactory屬性。
2) public ServletFileUpload(FileItemFactory fileItemFactory):
構造一個執行個體,并根據參數指定的FileItemFactory 對象,設定
fileItemFactory屬性。
ServletFileUpload類常用方法:
1. public void setSizeMax(long sizeMax)
方法setSizeMax方法繼承自FileUploadBase類,用于設定請求消息實體内容(即所有上傳資料)的最大尺寸限制,以防止用戶端惡意上傳超大檔案來浪費伺服器端的存儲空間。其參數是以位元組為機關的long型數字。
在請求解析的過程中,如果請求消息體内容的大小超過了setSizeMax方法的設定值,将會抛出FileUploadBase内部定義的SizeLimitExceededException異常(FileUploadException的子類)。
如:
org.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException:
the request was rejected because its size (1649104) exceeds the configured
maximum (153600)
該方法有一個對應的讀方法:public long getSizeMax()方法。
2. public void setFileSizeMax(long fileSizeMax)
方法setFileSizeMax方法繼承自FileUploadBase類,用于設定單個上傳檔案的最大尺寸限制,以防止用戶端惡意上傳超大檔案來浪費伺服器端的存儲空間。其參數是以位元組為機關的long型數字。
該方法有一個對應的讀方法:public long geFileSizeMax()方法。
在請求解析的過程中,如果單個上傳檔案的大小超過了setFileSizeMax方法的設定值,将會抛出FileUploadBase内部定義的FileSizeLimitExceededException異常(FileUploadException的子類)。
如:
org.apache.commons.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file1 exceeds its
maximum permitted size of 51200 characters.
3. public List parseRequest(javax.servlet.http.HttpServletRequest req)
parseRequest 方法是ServletFileUpload類的重要方法,它是對HTTP請求消息體内容進行解析的入口方法。它解析出FORM表單中的每個字段的資料,并将它們分别包裝成獨立的FileItem對象,然後将這些FileItem對象加入進一個List類型的集合對象中傳回。
該方法抛出FileUploadException異常來處理諸如檔案尺寸過大、請求消息中的實體内容的類型不是“multipart/form-data”、IO異常、請求消息體長度資訊丢失等各種異常。每一種異常都是FileUploadException的一個子類型。
4. public FileItemIterator getItemIterator(HttpServletRequest request)
getItemIterator方法和parseRequest 方法基本相同。
但是getItemIterator方法傳回的是一個疊代器,該疊代器中儲存的不是FileItem對象,而是FileItemStream 對象,如果你希望進一步提高性能,你可以采用 getItemIterator 方法,直接獲得每一個檔案項的資料輸入流,做底層處理;
如果性能不是問題,你希望代碼簡單,則采用parseRequest方法即可。
5. public stiatc boolean isMultipartContent(HttpServletRequest req)
isMultipartContent方法方法用于判斷請求消息中的内容是否是“multipart/form-data”類型,是則傳回true,否則傳回false。
isMultipartContent方法是一個靜态方法,不用建立ServletFileUpload類的執行個體對象即可被調用。
6. getFileItemFactory()和setFileItemFactory(FileItemFactory)
兩個方法繼承自FileUpload類,用于設定和讀取fileItemFactory屬性。
7. public void setProgressListener(ProgressListener pListener)
設定檔案上傳進度監聽器。該方法有一個對應的讀取方法:
ProgressListener getProgressListener()。
8.public void setHeaderEncoding()方法
在檔案上傳請求的消息體中,除了普通表單域的值是文本内容以外,檔案上傳字段中的檔案路徑名也是文本,在記憶體中儲存的是它們的某種字元集編碼的位元組數組,Apache檔案上傳元件在讀取這些内容時,必須知道它們所采用的字元集編碼,才能将它們轉換成正确的字元文本傳回。
setHeaderEncoding方法繼承自FileUploadBase類,用于設定上面提到的字元編碼。
如果沒有設定,則對應的讀方法getHeaderEncoding()方法傳回null,将采用HttpServletRequest設定的字元編碼,如果HttpServletRequest的字元編碼也為null,則采用系統預設字元編碼。
可以通過一下語句獲得系統預設字元編碼:
System.getProperty(“file.encoding”));
1.6、FileItemStream性能提升
以下是使用FileItemStream三次上傳一個70M檔案的時間:
檔案名為:電影.avi
Desc:這是資訊
用時:1516
檔案名為:電影.avi
Desc:這是資訊
用時:703
檔案名為:電影.avi
Desc:這是資訊
用時:656
對比:
以下是用FileItem三次上傳一個70M檔案的時間:
檔案名為:電影.avi
說明是:這是資訊
用時:1687
檔案名為:電影.avi
說明是:這是資訊
用時:2578
檔案名為:電影.avi
說明是:這是資訊
用時:2297
可見:FileItemSteam(servletFileUpload.getItemIterator(httpServletRequest))速度要快于FileItem(servletFileUpload.parseRequest(request))速度。
且一般情況下,FileItemSteam不産生臨時檔案:
源代碼:
package cn.itcast.servlet;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Scanner;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FileUtils;
/**
* FileItemStream示例
* @author <a href="mailto:[email protected]">王健</a>
* @version 1.0 2012-3-15
*/
public class Upload3Servlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long start = System.currentTimeMillis();
String path = getServletContext().getRealPath("/imgs");
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(1024 * 8);// 設定8k的緩存空間
factory.setRepository(new File("d:/a"));
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setHeaderEncoding("UTF-8");// 設定檔案名進行中文編碼
try {
FileItemIterator fii = upload.getItemIterator(request);// 使用周遊類
while (fii.hasNext()) {
FileItemStream fis = fii.next();
if (fis.isFormField()) {//FileItemStream同樣使用OpenStream擷取普通表單的值
InputStreamReader in = new InputStreamReader(fis.openStream(),"UTF-8");
Scanner sc = new Scanner(in);
StringBuffer sb = new StringBuffer();
if(sc.hasNextLine()){
sb.append(sc.nextLine());
}
System.err.println("Desc:"+sb.toString());
} else {
String fileName = fis.getName();
fileName = fileName
.substring(fileName.lastIndexOf("\\") + 1);
System.err.println("檔案名為:" + fileName);
InputStream in = fis.openStream();
FileUtils.copyInputStreamToFile(in, new File(path+"/"+fileName));
}
}
} catch (FileUploadException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.err.println("用時:"+(end-start));
}
}
1.7、FileCleanerCleanup清理資源
隻适用于你使用 DiskFileItem 的情況.換句話說,就是在你處理上傳的資料之前它們被存放在臨時檔案中。
這些臨時檔案在不再被使用的時候(如果相應的java.io.File是可回收的則更好)會自動被删除.
這會被org.apache.commons.io.FileCleaningTracker的一個執行個體啟動的一個收割線程默默執行。
你的web應用應該使用org.apache.commons.fileupload.FileCleanerCleanup的一個執行個體.那很簡單,你隻要把它加到你的 web.xml 中:
<listener>
<listener-class>org.apache.commons.fileupload.servlet.FileCleanerCleanup</listener-class>
</listener>
然後,在建立了DiskFileItemFactory以後,設定資源回收:
DiskFileItemFactory f = new DiskFileItemFactory();//聲明臨時檔案對象
f.setFileCleaningTracker(FileCleanerCleanup.getFileCleaningTracker(this.getServletContext()));
f.setSizeThreshold(1024*8); //設定在記憶體中最多可以放多少個位元組,如果超出這個位元組,儲存到臨檔案中去
f.setRepository(new File("d:/a")); //設定臨時目錄
注意:必須要正常關閉Tomcat伺服器。因為此線程在tomcat終止時會調用清空臨時檔案的代碼。
正常關閉,是指執行CATALINA_HOME\bin\shutdown.bat檔案。
1.8、進度ProgressListener
這個進度條比較合适于在背景監控進度,如果在作上傳進度,還是使用ajax更加合适:
upload.setProgressListener(new ProgressListener() {
double dd = 0;
long len = 0;
//參數1:已經上傳完成的數量
//參數2:總長度
//參數3:第幾個元素從1開始。0為沒有
public void update(long pBytesRead, long pContentLength, int pItems) {
double persent = pBytesRead*100/pContentLength;
if(dd!=persent){
System.err.println(dd+"%");
dd=persent;
}else if(persent==100){
System.err.println("100%");
}
}
});