一、 引言
基于瀏覽器的文件上傳,特別是對于通過<input type="file">標簽包含到Web頁面來實現(xiàn)上傳的情況,還存在較嚴重的性能問題。我們知道,超過10MB的上傳文件經(jīng)常導致一種非常痛苦的用戶體驗。一旦用戶提交了文件,在瀏覽器把文件上傳到
服務器的過程中,界面看上去似乎處于靜止狀態(tài)。由于這一切發(fā)生在后臺,所以許多沒有耐心的用戶開始認為服務器"掛"了,因而再次提交文件,這當然使得情況變得更糟糕。
為了盡可能使得文件上傳感覺更友好些,一旦用戶提交文件,許多站點將顯示一個中間過程動畫(例如一旋轉(zhuǎn)圖標)。盡管這一技術在上傳提交到服務器時起一些
作用,但它還是提供了太少的有關文件上傳狀態(tài)的信息。解決這個問題的另外一種嘗試是實現(xiàn)一個applet——它通過FTP把文件上傳到服務器。這一方案的
缺點是:限制了你的用戶,必須要有一個支持Java的瀏覽器。
在本文中,我們將實現(xiàn)一個具有
ajax(動態(tài)網(wǎng)站靜態(tài)化)能力的組件——它不僅實現(xiàn)把文件上傳到服務器,而且"實時地"監(jiān)視文件上傳的實際過程。
二、 實現(xiàn)該組件
首先,我們分析創(chuàng)建多部分過濾的過程,它將允許我們處理并且監(jiān)視文件上傳。然后,我們將繼續(xù)實現(xiàn)JavaServer Faces(
JSF)組件-它將提供給用戶連續(xù)的回饋,以支持ajax(動態(tài)網(wǎng)站靜態(tài)化)的進度條方式。
(一) 多部分過濾:UploadMultipartFilter
多部分過濾的任務是攔截到來的文件上傳并且把該文件寫到一個服務器上的臨時目錄中。同時,它還將監(jiān)視接收的字節(jié)數(shù)并且確定已經(jīng)上載該文件的程度。幸運的
是,現(xiàn)在有一個優(yōu)秀的Jakarta-Commons開源庫可以利用(FileUpload),可以由它來負責分析一個HTTP多部分請求并且把文件上傳
到服務器。我們要做的是擴展該庫并且加入我們需要的"鉤子"來監(jiān)視已經(jīng)處理了多少字節(jié)。
public class UploadMultipartFilter implements Filter{
public void doFilter(ServletRequest request,ServletResponse response,F(xiàn)ilterChain 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,F(xiàn)ileItem> 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,F(xiàn)ileItem>();
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){
//請求時間超過-用戶可能已經(jīng)轉(zhuǎn)到另一個頁面。
//作一些記錄
//...
}
...
在UploadMultipartRequestWrapper類中,我們將初始化ServletFileUpload類,它負責分析我們的請求并且把
文件寫到服務器上的缺省臨時目錄。ServletFileUpload實例針對在該請求中遇到的每一個字段創(chuàng)建一個FileItem實例(它們包含文件上
傳和正常的表單元素)。之后,一個FileItem實例用于檢索一個提交字段的屬性,或者,在文件上傳的情況下,檢索一個到底層的臨時文件的
InputStream。總之,UploadMultipartRequestWrapper負責分析該文件并且設置任何FileItem-它在該請求中
把文件上傳描述為屬性。然后,這些屬性由JSF組件所進一步收集,而正常表單字段的行為保持不變。
默認情況下,通用
FileUpload庫將使用DiskFileItems類的實例來處理文件上傳。盡管DiskFileItem在處理整個臨時文件業(yè)務時是很有用的,但
在準確監(jiān)視該文件已經(jīng)處理程度方面存在很少支持。自版本1.1以來,通用FileUpload庫能夠使開發(fā)者指定用于創(chuàng)建FileItem的工廠。我們將
使用ProgressMonitorFileItemFactory和ProgressMonitorFileItem類來重載缺省行為并監(jiān)視文件上傳過
程。
(三) 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頭由瀏覽器設置并且假定它是被設置的上傳文件的精確長度。這種確定文件長度的方法確實限制了你在每次請求中上傳的文件-如果
有多個文件在該請求中被編碼的話,不過這個值是不精確的。這是由于,瀏覽器僅僅發(fā)送一個Content-Length頭,而不考慮上傳的文件數(shù)目。
除了創(chuàng)建ProgressMonitorFileItem實例之外,ProgressMonitorFileItemFactory還注冊了一個
ProgressObserver實例,它將由ProgressMonitorFileItem來發(fā)送文件上傳過程中的更新。我們所使用的
ProgressObserver的實現(xiàn)(SessionUpdatingProgressObserver)針對被提交字段的id把進度百分數(shù)設置到用
戶的會話中。然后,這個值可以由JSF組件存取以便把更新發(fā)送給用戶。
(四) ProgressMonitorFileItem類
public class ProgressMonitorFileItem extends DiskFileItem {
private ProgressObserver observer;
private long passedInFileSize;
...
private boolean isFormField;
...
@Override
public OutputStream getOutputStream() throws IOException {
OutputStream baseOutputStream = super.getOutputStream();
if(isFormField == false){
return new BytesCountingOutputStream(baseOutputStream);
}else{return baseOutputStream; }
}
...
private class BytesCountingOutputStream extends OutputStream{
private long previousProgressUpdate;
private OutputStream base;
public BytesCountingOutputStream(OutputStream ous){ base = ous; }
...
private void fireProgressEvent(int b){
bytesRead += b;
...
double progress = (((double)(bytesRead)) / passedInFileSize);
progress *= 100.0
observer.setProgress();
}
}
}
ProgressMonitorFileItem把DiskFileItem的缺省OutputStream包裝到一個BytesCountingOutputStream中,這可以在每次讀取一定數(shù)目的字節(jié)后更新相關的ProgressObserver。
(五) 支持ajax(動態(tài)網(wǎng)站靜態(tài)化)的JavaServer Faces(JSF)上傳組件
這個組件負責生成HTML文件上傳標簽,顯示一個進度條以監(jiān)視文件上傳,并且生成一旦文件上傳成功需要被顯示的組件。使用JavaServer
Faces實現(xiàn)這個組件的一個主要優(yōu)點是,大多數(shù)復雜性被隱藏起來。開發(fā)人員只需要把組件標簽添加到jsp(SUN企業(yè)級應用的首選),而后由組件負責所
有的ajax(動態(tài)網(wǎng)站靜態(tài)化)及相關的進度條監(jiān)控細節(jié)問題。下面的jsp(SUN企業(yè)級應用的首選)代碼片斷用于把上傳組件添加到頁面上。
<comp:fileUpload
value="#{uploadPageBean.uploadedFile}"
uploadIcon="images/upload.png"
styleClass="progressBarDiv"
progressBarStyleClass="progressBar"
cellStyleClass="progressBarCell"
activeStyleClass="progressBarActiveCell">
<%--下面是一旦文件上傳完成將成為可見的組件--%>
<h:panelGrid columns="2" cellpadding="2" cellspacing="0" width="100%">
<f:facet name="header">
<h:outputText styleClass="text"
value="文件上傳成功." />
</f:facet>
<h:panelGroup style="text-align:left; display:block; width:100%; ">
<h:commandButton action="#{uploadPageBean.reset}"
image="images/reset.png"/>
</h:panelGroup>
<h:panelGroup style="text-align:right; display:block; width:100%; ">
<h:commandButton action="#{uploadPageBean.nextPage}"
image="images/continue.png"/>
</h:panelGroup>
</h:panelGrid>
</comp:fileUpload>
文件上傳組件的value屬性需要用一個擁有一個FileItem的屬性綁定到一個bean上。組件只有在該文件被服務器成功收到時才顯示。
三、 實現(xiàn)ajax(動態(tài)網(wǎng)站靜態(tài)化)文件上傳組件
實質(zhì)上,上載組件或者生成一個完整的自已,或者在一個ajax(動態(tài)網(wǎng)站靜態(tài)化)請求的情況下,只生成部分xml(標準化越來越近了)以更新在頁面上進
度條的狀態(tài)。為了防止JavaServer
Faces生成完整的組件樹(這會帶來不必要的負荷),我們還需要實現(xiàn)一個PhaseListener(PagePhaseListener)以取消該
faces的請求處理的其它部分-如果遇到一個ajax(動態(tài)網(wǎng)站靜態(tài)化)請求的話。我在本文中略去了所有的關于標準配置(faces-
config.xml(標準化越來越近了)和標簽庫)的討論,因為它們相當直接且已經(jīng)在以前討論過;而且這一切都包含在隨同本文的源碼中,你可以詳細分
析。
(一) ajax(動態(tài)網(wǎng)站靜態(tài)化)文件上傳組件生成器
該組件和標簽類的實現(xiàn)比較簡單。大量的邏輯被包含到生成器中,具體地說,它負責以下:
· 編碼整個的上傳組件(和完整的HTML文件上傳標簽)、文件被上傳完成后要顯示的組件,還有實現(xiàn)ajax(動態(tài)網(wǎng)站靜態(tài)化)請求的客戶端JavaScript代碼。
· 適當?shù)靥幚聿糠謅jax(動態(tài)網(wǎng)站靜態(tài)化)請求并且發(fā)送回必要的xml(標準化越來越近了)。
· 解碼一個文件上傳并且把它設置為一個FileItem實例。
(二) 編碼整個上傳組件
前面已經(jīng)提及,文件上傳組件由三個階段組成。在該組件的整個編碼期間,我們將詳細分析這三個階段的編碼。注意,在頁面上的該組件的可視化(使用CSS顯示)屬性將由ajax(動態(tài)網(wǎng)站靜態(tài)化) JavaScript來控制。
(三) 階段一
圖5顯示了該上傳組件的第一個階段。
圖5.選擇文件上傳
在第一階段中,我們需要生成HTML文件Upload標簽和點擊Upload按鈕時相應的執(zhí)行代碼。一旦用戶點擊了Upload按鈕,表單將被一個IFRAME(為防止頁面阻塞)提交并初始化第二個階段。下面是生成代碼的一部分:
//文件上傳組件
writer.startElement("input", component);
writer.writeAttribute("type", "file", null);
writer.writeAttribute("name", component.getClientId(context), "id");
writer.writeAttribute("id", component.getClientId(context),"id");
if(input.getValue() != null){
//如果可用,則生成該文件名.
FileItem fileData = (FileItem)input.getValue();
writer.writeAttribute("value", fileData.getName(), fileData.getName());
}
writer.endElement("input");
String iconURL = input.getUploadIcon();
//生成圖像,并把JavaScript事件依附到其上.
writer.startElement("div", component);
writer.writeAttribute("style","display:block; width:100%; text-align:center; ", "style");
writer.startElement("img", component);
writer.writeAttribute("src",iconURL,"src");
writer.writeAttribute("type","image","type");
writer.writeAttribute("style","cursor:hand; cursor:pointer; ","style");
UIForm form = FacesUtils.getForm(context,component);
if(form != null) {
String getFormJS = "document.getElementById('" + form.getClientId(context) + "')";
String jsFriendlyClientID = input.getClientId(context).replace(":","_");
//設置表單的編碼為multipart以用于文件上傳,并且通過一個IFRAME
//來提交它的內(nèi)容。該組件的第二個階段也在500毫秒后被初始化.
writer.writeAttribute("onclick",getFormJS + ".encoding='multipart/form-data'; " +
getFormJS + ".target='" + iframeName + "'; " + getFormJS + ".submit(); " +
getFormJS + ".encoding='application/x-www-form-urlencoded'; " +
getFormJS + ".target='_self'; " +
"setTimeout('refreshProgress" + jsFriendlyClientID + "(); ',500); ",null);
}
...
writer.endElement("img");
//現(xiàn)在實現(xiàn)我們將要把該文件/表單提交到的IFRAME.
writer.startElement("iframe", component);
writer.writeAttribute("id", iframeName, null);
writer.writeAttribute("name",iframeName,null);
writer.writeAttribute("style","display:none; ",null);
writer.endElement("iframe");
writer.endElement("div");
writer.endElement("div"); //階段1結(jié)束
(四) 階段二
第二階段是顯示當前百分比的進度條和標簽,如圖6所示。該進度條是作為一個具有100個內(nèi)嵌span標簽的div標簽實現(xiàn)的。這些將由ajax(動態(tài)網(wǎng)站靜態(tài)化) JavaScript根據(jù)來自于服務器的響應進行設置。
圖6.上傳文件到服務器
writer.startElement("div",component);
writer.writeAttribute("id", input.getClientId(context) + "_stage2", "id");
...
writer.writeAttribute("style","display:none", "style");
String progressBarID = component.getClientId(context) + "_progressBar";
String progressBarLabelID = component.getClientId(context) + "_progressBarlabel";
writer.startElement("div", component);
writer.writeAttribute("id",progressBarID,"id");
String progressBarStyleClass = input.getProgressBarStyleClass();
if(progressBarStyleClass != null)
writer.writeAttribute("class",progressBarStyleClass,"class");
for(int i=0; i<100; i++){
writer.write("<span> </span>");
}
writer.endElement("div");
writer.startElement("div",component);
writer.writeAttribute("id",progressBarLabelID,"id");
...
writer.endElement("div");
writer.endElement("div"); //階段2結(jié)束
(五) 階段三
最后,作為階段三,一旦文件成功上傳,需要被顯示的組件即被生成,見圖7。這些是在生成器的encodeChildren方法中實現(xiàn)的。
圖7.上傳完成
public void encodeChildren(FacesContext context,
UIComponent component) throws IOException {
ResponseWriter writer = context.getResponseWriter();
UIFileUpload input = (UIFileUpload)component;
//一旦文件上傳成功,處理將被顯示的子結(jié)點
writer.startElement("div", component);
writer.writeAttribute("id", input.getClientId(context) + "_stage3", "id"); //階段3.
if(input.getValue() == null){
writer.writeAttribute("style","display:none; ",null);
}else{
writer.writeAttribute("style","display:block",null);
}
List<UIComponent> children = input.getChildren();
for(UIComponent child : children){
FacesUtils.encodeRecursive(context,child);
}
writer.endElement("div"); //階段3結(jié)束
}
四、處理ajax(動態(tài)網(wǎng)站靜態(tài)化)請求
ajax(動態(tài)網(wǎng)站靜態(tài)化)請求的生成是在這個組件的解碼方法中處理的。我們需要檢查這是否是一個實際的ajax(動態(tài)網(wǎng)站靜態(tài)化)請求(為了區(qū)別于正
常的編譯行為),然后基于由ProgressMonitorFileItemFactory類的
SessionUpdatingProgressObserver實例設置在會話中的值把一個xml(標準化越來越近了)響應發(fā)送回客戶端。
public void decode(FacesContext context, UIComponent component) {
UIFileUpload input = (UIFileUpload) component;
//檢查是否這是一個上傳進度請求,或是一個實際的上傳請求.
ExternalContext extContext = context.getExternalContext();
Map parameterMap = extContext.getRequestParameterMap();
String clientId = input.getClientId(context);
Map requestMap = extContext.getRequestParameterMap();
if(requestMap.get(clientId) == null){
return; //什么也不做,返回
}
if(parameterMap.containsKey(PROGRESS_REQUEST_PARAM_NAME)){
//這是一個在該文件請求中的得到進度信息的請求.
//得到該進度信息并把它生成為xml(標準化越來越近了)
HttpServletResponse response = (HttpServletResponse)context.getExternalContext().getResponse();
//設置響應的頭信息
response.setContentType("text/xml(標準化越來越近了)");
response.setHeader("Cache-Control", "no-cache");
try {
ResponseWriter writer = FacesUtils.setupResponseWriter(context);
writer.startElement("progress", input);
writer.startElement("percentage", input);
//從會話中獲得當前進度百分數(shù)(由過濾器所設置).
Double progressCount = (Double)extContext.getSessionMap().
get("FileUpload.Progress." +input.getClientId(context));
if(progressCount != null){
writer.writeText(progressCount, null);
}else{
writer.writeText("1", null); //我們還沒有收到上傳
}
writer.endElement("percentage");
writer.startElement("clientId", input);
writer.writeText(input.getClientId(context), null);
writer.endElement("clientId");
writer.endElement("progress");
} catch(Exception e){
//做一些錯誤記錄...
}
}else{
//正常的譯碼請求.
...
五、 正常的譯碼行為
在正常的編譯期間,文件上傳生成器從請求屬性中檢索FileItem,正是在此處它被過濾器所設置,并且更新該組件的值綁定。然后,該會話中的進度被更新到100%,這樣在頁面上的JavaScript就可以把組件送入第3個階段。
//正常的譯碼請求.
if(requestMap.get(clientId).toString().equals("file")){
try{
HttpServletRequest request = (HttpServletRequest)extContext.getRequest();
FileItem fileData = (FileItem)request.getAttribute(clientId);
if(fileData != null) input.setSubmittedValue(fileData);
//現(xiàn)在我們需要清除與該項相關的任何進度
extContext.getSessionMap().put("FileUpload.Progress." + input.getClientId(context),new Double(100));
}catch(Exception e){
throw new RuntimeException("不能處理文件上傳" +" - 請配置過濾器.",e);
}
}
客戶端JavaScript負責向服務器發(fā)出進度請求并通過不同階段來移動組件。為了簡化處理所有的瀏覽器特定的xml(標準化越來越近了)
HttpRequest對象的問題,我選用了Matt
Krause提供的ajax(動態(tài)網(wǎng)站靜態(tài)化)Request.js庫。該庫最大限度地減少我們需要編寫的JavaScript代碼的數(shù)量,同時可以使這
個組件正常工作。也許把這部分JavaScript代碼打包為該組件的一部分,然后從PhaseListener生成它更好一些,但是,我已經(jīng)通過定義一
個到jsp(SUN企業(yè)級應用的首選)頁面上的JavaScript庫的鏈接來盡力使得它簡單。
組件中的
getProgressBarJavaScript方法被調(diào)用以生成JavaScript。使JavaScript正常工作通常是實現(xiàn)ajax(動態(tài)網(wǎng)站
靜態(tài)化)組件最困難的部分;不過我想,下面的代碼已經(jīng)非常清晰易于理解了。盡管在我的示例中JavaScript是嵌入到Java代碼中的,但是把它放到
一個外部獨立的文件中也許更好一些。在本文中,我只是想使問題更為簡單些且只關心本文的主題。下面是一個將由組件生成的JavaScript的示例。其中
假定,fileUpload1是被賦值到該文件組件的客戶端JSF Id,而uploadForm是HTML表單的Id。
function refreshProgress(){
// 假定我們正在進入到階段2.
document.getElementById('fileUpload1_stage1').style.display = 'none';
document.getElementById('fileUpload1_stage2').style.display = '';
document.getElementById('fileUpload1_stage3').style.display = 'none';
//創(chuàng)建ajax(動態(tài)網(wǎng)站靜態(tài)化)寄送
ajax(動態(tài)網(wǎng)站靜態(tài)化)Request.post(
{
//指定正確的參數(shù),以便
//該組件在服務器端被正確處理
'parameters':{ 'uploadForm':'uploadForm',
'fileUpload1':'fileUpload1',
'jsf.component.UIFileUpload':'1',
'ajax(動態(tài)網(wǎng)站靜態(tài)化).abortPhase':'4' } //Abort at Phase 4.
//指定成功處理相應的回調(diào)方法.
,'onSuccess':function(req) {
var xml(標準化越來越近了) = req.responsexml(標準化越來越近了);
if( xml(標準化越來越近了).getElementsByTagName('clientId').length == 0) {
setTimeout('refreshProgress()',200); return;
}
var clientId = xml(標準化越來越近了).getElementsByTagName('clientId');
clientId = clientId[0].firstChild.nodeValue + '_progressBar';
//從xml(標準化越來越近了)獲取百分比
var percentage = xml(標準化越來越近了).getElementsByTagName('percentage')[0].firstChild.nodeValue;
var innerSpans = document.getElementById(clientId).getElementsByTagName('span');
document.getElementById(clientId + 'label').innerHTML = Math.round(percentage) + '%';
//基于當前進度,設置這些span的式樣類。
for(var i=0; i<innerSpans.length; i++){
if(i < percentage){
innerSpans[i].className = 'active';
}else{
innerSpans[i].className = 'passive';
}
}
//如果進度不是100,我們需要繼續(xù)查詢服務器以實現(xiàn)更新.
if(percentage != 100){
setTimeout('refreshProgress()',400);
} else {
//文件上傳已經(jīng)完成,我們現(xiàn)在需要把該組件送入到第3個階段.
document.getElementById('fileUpload1_stage1').style.display = 'none';
document.getElementById('fileUpload1_stage2').style.display = 'none';
document.getElementById('fileUpload1_stage3').style.display = '';
}
}
});
}
return builder.toString();
六、 結(jié)論
我很希望,本文能夠在有關如何使得文件上傳更具有用戶友好性,并且把ajax(動態(tài)網(wǎng)站靜態(tài)化)和JavaServer
Faces用于實現(xiàn)高級用戶接口組件的可能性方面引發(fā)你的進一步思考。毫無疑問,本文中的方案比較冗長并且有可能得到進一步的改進。我希望你能詳細地分析
一下本文中所提供的完整的源代碼來深入理解本文中所討論的概念。