当前页面: 开发资料首页 → JSP 专题 → AJAX+JSF组件实现高性能的文件上载
摘要: AJAX+JSF组件实现高性能的文件上载
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/continue.png"/>
</h:panelGroup>
</h:panelGrid>
</comp:fileUpload>
文件上传组件的value属性需要用一个拥有一个FileItem的属性绑定到一个bean上。组件只有在该文件被服务器成功收到时才显示。
三、 实现AJAX文件上传组件
实质上,上载组件或者生成一个完整的自已,或者在一个AJAX请求的情况下,只生成部分XML以更新在页面上进度条的状态。为了防止JavaServer Faces生成完整的组件树(这会带来不必要的负荷),我们还需要实现一个PhaseListener(PagePhaseListener)以取消该faces的请求处理的其它部分-如果遇到一个AJAX请求的话。我在本文中略去了所有的关于标准配置(faces-config.xml和标签库)的讨论,因为它们相当直接且已经在以前讨论过;而且这一切都包含在随同本文的源码中,你可以详细分析。
(一) AJAX文件上传组件生成器
该组件和标签类的实现比较简单。大量的逻辑被包含到生成器中,具体地说,它负责以下:
· 编码整个的上传组件(和完整的HTML文件上传标签)、文件被上传完成后要显示的组件,还有实现AJAX请求的客户端JavaScript代码。
· 适当地处理部分AJAX请求并且发送回必要的XML。
· 解码一个文件上传并且把它设置为一个FileItem实例。
(二) 编码整个上传组件
前面已经提及,文件上传组件由三个阶段组成。在该组件的整个编码期间,我们将详细分析这三个阶段的编码。注意,在页面上的该组件的可视化(使用CSS显示)属性将由AJAX JavaScript来控制。
(三) 阶段一
图5显示了该上传组件的第一个阶段。
图5.选择文件上传
在第一阶段中,我们需要生成HTML文件Upload标签和点击Upload按钮时相应的执行代码。一旦用户点击了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
//来提交它的内容。该组件的第二个阶段也在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");
//现在实现我们将要把该文件/表单提交到的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结束
(四) 阶段二
第二阶段是显示当前百分比的进度条和标签,如图6所示。该进度条是作为一个具有100个内嵌span标签的div标签实现的。这些将由AJAX JavaScript根据来自于服务器的响应进行设置。
图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结束
(五) 阶段三
最后,作为阶段三,一旦文件成功上传,需要被显示的组件即被生成,见图7。这些是在生成器的encodeChildren方法中实现的。
图7.上传完成
public void encodeChildren(FacesContext context,
UIComponent component) throws IOException {
ResponseWriter writer = context.getResponseWriter();
UIFileUpload input = (UIFileUpload)component;
//一旦文件上传成功,处理将被显示的子结点
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结束
}
四、处理AJAX请求
AJAX请求的生成是在这个组件的解码方法中处理的。我们需要检查这是否是一个实际的AJAX请求(为了区别于正常的编译行为),然后基于由ProgressMonitorFileItemFactory类的SessionUpdatingProgressObserver实例设置在会话中的值把一个XML响应发送回客户端。
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);
//从会话中获得当前进度百分数(由过滤器所设置).
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);
//现在我们需要清除与该项相关的任何进度
extContext.getSessionMap().put("FileUpload.Progress." + input.getClientId(context),new Double(100));
}catch(Exception e){
throw new RuntimeException("不能处理文件上传" +" - 请配置过滤器.",e);
}
}
客户端JavaScript负责向服务器发出进度请求并通过不同阶段来移动组件。为了简化处理所有的浏览器特定的XMLHttpRequest对象的问题,我选用了Matt Krause提供的AjaxRequest.js库。该库最大限度地减少我们需要编写的JavaScript代码的数量,同时可以使这个组件正常工作。也许把这部分JavaScript代码打包为该组件的一部分,然后从PhaseListener生成它更好一些,但是,我已经通过定义一个到JSP页面上的JavaScript库的链接来尽力使得它简单。
组件中的getProgressBarJavaScript方法被调用以生成JavaScript。使JavaScript正常工作通常是实现AJAX组件最困难的部分;不过我想,下面的代码已经非常清晰易于理解了。尽管在我的示例中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';
//创建AJAX寄送
AjaxRequest.post(
{
//指定正确的参数,以便
//该组件在服务器端被正确处理
'parameters':{ 'uploadForm':'uploadForm',
'fileUpload1':'fileUpload1',
'jsf.component.UIFileUpload':'1',
'ajax.abortPhase':'4' } //Abort at Phase 4.
//指定成功处理相应的回调方法.
,'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,我们需要继续查询服务器以实现更新.
if(percentage != 100){
setTimeout('refreshProgress()',400);
} else {
//文件上传已经完成,我们现在需要把该组件送入到第3个阶段.
document.getElementById('fileUpload1_stage1').style.display = 'none';
document.getElementById('fileUpload1_stage2').style.display = 'none';
document.getElementById('fileUpload1_stage3').style.display = '';
}
}
});
}
return builder.toString();
六、 结论
我很希望,本文能够在有关如何使得文件上传更具有用户友好性,并且把AJAX和JavaServer Faces用于实现高级用户接口组件的可能性方面引发你的进一步思考。毫无疑问,本文中的方案比较冗长并且有可能得到进一步的改进。我希望你能详细地分析一下本文中所提供的完整的源代码来深入理解本文中所讨论的概念。</td>
</tr>
</table>
↑返回目录
前一篇: 谈select列表框
后一篇: 快”在细节 J2EE程序的性能优化技巧