一、 引言
基于浏览器的文件上传,特别是对于通过<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组件存取以便把更新发送给用户。
(四) 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中,这可以在每次读取一定数目的字节后更新相关的ProgressObserver。
(五) 支持AJAX的JavaServer Faces(JSF)上传组件
这个组件负责生成HTML文件上传标签,显示一个进度条以监视文件上传,并且生成一旦文件上传成功需要被显示的组件。使用JavaServer Faces实现这个组件的一个主要优点是,大多数复杂性被隐藏起来。开发人员只需要把组件标签添加到JSP,而后由组件负责所有的AJAX及相关的进度条监控细节问题。下面的JSP代码片断用于把上传组件添加到页面上。
<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/