一、 引言
基於浏覽器的文件上傳,特別是對於通過<input type="file">標簽包含到Web頁面來實現上傳的情況,還存在較嚴重的性能問題。我們知道,超過10MB的上傳文件經常導致一種非常痛苦的用戶體驗。一旦用戶提交了文件,在浏覽器把文件上傳到服務器的過程中,界面看上去似乎處於靜止狀態。由於這一切發生在後台,所以許多沒有耐心的用戶開始認為服務器"掛"了,因而再次提交文件,這當然使得情況變得更糟糕。
為了盡可能使得文件上傳感覺更友好些,一旦用戶提交文件,許多站點將顯示一個中間過程動畫(例如一旋轉圖標)。盡管這一技術在上傳提交到服務器時起一些作用,但它還是提供了太少的有關文件上傳狀態的信息。解決這個問題的另外一種嘗試是實現一個applet——它通過FTP把文件上傳到服務器。這一方案的缺點是:限制了你的用戶,必須要有一個支持Java的浏覽器。
在本文中,我們將實現一個具有AJax能力的組件——它不僅實現把文件上傳到服務器,而且"實時地"監視文件上傳的實際過程。這個組件工作的四個階段顯示於下面的圖1,2,3和4中:
圖1.階段1:選擇文件上傳
圖2.階段2:上傳該文件到服務器
圖3.階段3:上傳完成
圖4.階段4:文件上傳摘要
二、 實現該組件 首先,我們分析創建多部分過濾的過程,它將允許我們處理並且監視文件上傳。然後,我們將繼續實現JavaServer Faces(JSF)組件-它將提供給用戶連續的回饋,以支持AJax的進度條方式。
(一) 多部分過濾:UploadMultipartFilter
多部分過濾的任務是攔截到來的文件上傳並且把該文件寫到一個服務器上的臨時目錄中。同時,它還將監視接收的字節數並且確定已經上載該文件的程度。幸運的是,現在有一個優秀的Jakarta-Commons開源庫可以利用(FileUpload),可以由它來負責分析一個HTTP多部分請求並且把文件上傳到服務器。我們要做的是擴展該庫並且加入我們需要的"鉤子"來監視已經處理了多少字節。
public class UploadMultipartFilter implements Filter{
public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
throws IOException, ServletException {
HttpServletRequest hRequest = (HttpServletRequest)request;
//檢查是否我們在處理一個多部分請求
String contentHeader = hRequest.getHeader("content-type");
boolean isMultipart = ( contentHeader != null && contentHeader.indexOf("multipart/form-data") != -1);
if(isMultipart == false){
chain.doFilter(request,response);
}else{
UploadMultipartRequestWrapper wrapper = new UploadMultipartRequestWrapper(hRequest);
chain.doFilter(wrapper,response);
}
...
}
正如你所見,UploadMultipartFilter類簡單地檢查了當前的請求是否是一個多部分請求。如果該請求不包含文件上傳,該請求將被傳遞到請求鏈中的下一個過濾,而不進行任何另外的處理。否則,該請求將被包裝在一個UploadMultipartRequestWrapper中。
(二) UploadMultipartRequestWrapper類
public class UploadMultipartRequestWrapper
extends HttpServletRequestWrapper{
private Map<String,String> formParameters;
private Map<String,FileItem> fileParameters;
public UploadMultipartRequestWrapper(HttpServletRequest request) {
super(request);
try{
ServletFileUpload upload = new ServletFileUpload();
upload.setFileItemFactory(new ProgressMonitorFileItemFactory(request));
List fileItems = upload.parseRequest(request);
formParameters = new HashMap<String,String>();
fileParameters = new HashMap<String,FileItem>();
for(int i=0;i<fileItems.size();i++){
FileItem item = (FileItem)fileItems.get(i);
if(item.isFormFIEld() == true){
formParameters.put(item.getFIEldName(),item.getString());
}else{
fileParameters.put(item.getFIEldName(),item);
request.setAttribute(item.getFIEldName(),item);
}
}
}catch(FileUploadException fe){
//請求時間超過-用戶可能已經轉到另一個頁面。
//作一些記錄
//...
}
...
在UploadMultipartRequestWrapper類中,我們將初始化ServletFileUpload類,它負責分析我們的請求並且把文件寫到服務器上的缺省臨時目錄。ServletFileUpload實例針對在該請求中遇到的每一個字段創建一個FileItem實例(它們包含文件上傳和正常的表單元素)。之後,一個FileItem實例用於檢索一個提交字段的屬性,或者,在文件上傳的情況下,檢索一個到底層的臨時文件的InputStream。總之,UploadMultipartRequestWrapper負責分析該文件並且設置任何FileItem-它在該請求中把文件上傳描述為屬性。然後,這些屬性由JSF組件所進一步收集,而正常表單字段的行為保持不變。
默認情況下,通用FileUpload庫將使用DiskFileItems類的實例來處理文件上傳。盡管DiskFileItem在處理整個臨時文件業務時是很有用的,但在准確監視該文件已經處理程度方面存在很少支持。自版本1.1以來,通用FileUpload庫能夠使開發者指定用於創建FileItem的工廠。我們將使用ProgressMonitorFileItemFactory和ProgressMonitorFileItem類來重載缺省行為並監視文件上傳過程。
(三) ProgressMonitorFileItemFactory類
public class ProgressMonitorFileItemFactory extends DiskFileItemFactory {
private File temporaryDirectory;
private HttpServletRequest requestRef;
private long requestLength;
public ProgressMonitorFileItemFactory(HttpServletRequest request) {
super();
temporaryDirectory = (File)request.getSession().getServletContext().getAttribute("Javax.servlet.context.tempdir");
requestRef = request;
String contentLength = request.getHeader("content-length");
if(contentLength != null){requestLength = Long.parseLong(contentLength.trim());}
}
public FileItem createItem(String fieldName, String contentType,boolean isFormFIEld, String fileName) {
SessionUpdatingProgressObserver observer = null;
if(isFormFIEld == false) //這必須是一文件上傳.
observer = new SessionUpdatingProgressObserver(fIEldName,fileName);
ProgressMonitorFileItem item = new ProgressMonitorFileItem(
fieldName,contentType,isFormFIEld,
fileName,2048,temporaryDirectory,
observer,requestLength);
return item;
}
...
public class SessionUpdatingProgressObserver implements ProgressObserver {
private String fIEldName;
private String fileName;
...
public void setProgress(double progress) {
if(request != null){
request.getSession().setAttribute("FileUpload.Progress."+fIEldName,progress);
request.getSession().setAttribute("FileUpload.FileName."+fIEldName,fileName);
}
}
}
}
ProgressMonitorFileItemFactory Content-Length頭由浏覽器設置並且假定它是被設置的上傳文件的精確長度。這種確定文件長度的方法確實限制了你在每次請求中上傳的文件-如果有多個文件在該請求中被編碼的話,不過這個值是不精確的。這是由於,浏覽器僅僅發送一個Content-Length頭,而不考慮上傳的文件數目。
除了創建ProgressMonitorFileItem實例之外,ProgressMonitorFileItemFactory還注冊了一個ProgressObserver實例,它將由ProgressMonitorFileItem來發送文件上傳過程中的更新。我們所使用的ProgressObserver的實現(SessionUpdatingProgressObserver)針對被提交字段的id把進度百分數設置到用戶的會話中。然後,這個值可以由JSF組件存取以便把更新發送給用戶。