虽然上文中完成Upload工作的是Apache的Common-FileUpload组件,但在其代码中所使用的FileUpload1.1版本并没有1.2版本所提供的上传处理Listener功能,这就对检测文件上传情况造成了困难。我想正是这个原因致使Pierre-Alexandre使用了DWR+MonitoredDiskFileItem、MonitoredDiskFileItemFactory类(分别继承DiskFileItem、DiskFileItemFactory)的方式:前者负责在web客户端进行Remote Call;后者在进行文件数据读取时统计数据总量、读取数据量、处理文件总数,并保存于Session中,以供web客户端通过DWR远程调用UploadMonitor类的getUploadInfo方法进行轮询(Poll)。
从本人观点出发,Pierre-Alexandre实现的不足之处:
1.没有用户取消上传功能;
2.完全的DWR实现,没有使用Prototype,对于不会使用DWR的开发者来讲有一定的知识局限性,而且由于DWR的个性而造成不便将此实现集成到项目中。
Prototype+Servlet的实现:
所以出于研究Prototype之目的,本人经过仔细思考,尝试实现了一个Prototype+Servlet的简单Example。其工作流程很简单:
1.在Form提交上传文件Field的同时,使用AJAX周期性地从Servlet轮询上传状态信息;
2.然后,根据此信息更新进度条和相关文字,及时反映文件传输状态;
3.如果用户取消上传操作,则进行相应的现场清理工作:删除已经上传的文件,在Form提交页面中显示相关信息;
4.如果上传完毕,在Form提交页面中显示已经上传的文件内容(或链接),也可以与一些AJAX
SlideShow应用结合在一起。
服务器端代码:
Bean序列化/反序列化工作:XmlUnSerializer这个类虽然不能够通吃任何模样的Bean,但应付一般的Bean、具有Collection类型属性的Bean和Bean List来讲还是够用的。
{XmlUnSerializer类的核心方法serializeBean和serializeBeanList}:
/** |
//上传总量
private long
uploadTotalSize=0;
//读取上传总量
private long
readTotalSize=0;
//当前上传文件号
private int
currentUploadFileNum=0;
//成功读取上传文件数
private int
successUploadFileCount=0;
//状态
private String
status="";
//处理起始时间
private long
processStartTime=0l;
//处理终止时间
private long
processEndTime=0l;
//处理执行时间
private long
processRunningTime=0l;
//上传文件URL列表
private List
uploadFileUrlList=new ArrayList();
//取消上传
private boolean
cancel=false;
//上传base目录
private String
baseDir="";
文件上传状态监视工作:使用Common-FileUpload
1.2版本(20070103)。此版本与1.1版的区别在于提供了能够监视文件上传情况的ProcessListener接口,使开发者通过FileUploadBase类对象的setProcessListener方法植入自己的Listener,而且实现这个Listener很简单。
{FileUploadListener主要方法update}:
/**
* 更新状态
* @param pBytesRead 读取字节总数
* @param pContentLength 数据总长度
* @param pItems 当前正在被读取的field号
*/
public void update(long pBytesRead, long pContentLength, int
pItems){
FileUploadStatus
fuploadStatus=BackGroundService.takeOutFileUploadStatusBean(this.session);
logger.debug("当前正在处理第" + pItems+"个文件");
fuploadStatus.setUploadTotalSize(pContentLength);
//读取完成
if (pContentLength == -1) {
logger.debug("读取完成:读取了 " + pBytesRead + "
bytes.");
fuploadStatus.setStatus("完成对" +
pItems+"个文件的读取:读取了 " + pBytesRead + " bytes.");
fuploadStatus.setReadTotalSize(pBytesRead);
fuploadStatus.setSuccessUploadFileCount(pItems);
fuploadStatus.setProcessEndTime(System.currentTimeMillis());
fuploadStatus.setProcessRunningTime(fuploadStatus.getProcessEndTime());
//读取中
} else {
logger.debug("读取进行中:已经读取了 " +
pBytesRead + " / " + pContentLength+ " bytes.");
fuploadStatus.setStatus("当前正在处理第" +
pItems+"个文件:已经读取了 " + pBytesRead
+ " / " + pContentLength+ " bytes.");
fuploadStatus.setReadTotalSize(pBytesRead);
fuploadStatus.setCurrentUploadFileNum(pItems);
fuploadStatus.setProcessRunningTime(System.currentTimeMillis());
}
BackGroundService.storeFileUploadStatusBean(this.session,fuploadStatus);
}
很清楚,我也把FileUploadStatus这个Bean存取于Session中。
Servlet实现:BackGroundService这个Servlet类负责接收Form
Post数据、回应状态轮询请求、处理取消文件上传的请求。尽管可以把这些功能相互分离开来(比如构造一个FileUploadManager类),但出于简单明了、便于阅读之目的,还是将它们放到Servlet中,只是由不同的方法进行分割。
{BackGroundService中的processFileUpload方法用于处理文件上传请求}:
//取消上传
if
(takeOutFileUploadStatusBean(request.getSession()).getCancel()){
deleteUploadedFile(request);
break;
}
//保存文件
else
if (!item.isFormField() &&
item.getName().length()>0){
String
fileName=takeOutFileName(item.getName());
logger.debug("处理文件["+fileName+"]:保存路径为"
+request.getRealPath(UPLOAD_DIR)+File.separator+fileName);
File
uploadedFile =
new
File(request.getRealPath(UPLOAD_DIR)+File.separator+fileName);
item.write(uploadedFile);
//更新上传文件列表
FileUploadStatus
fUploadStatus=takeOutFileUploadStatusBean
(request.getSession());
fUploadStatus.getUploadFileUrlList().add(fileName);
storeFileUploadStatusBean(request.getSession(),fUploadStatus);
Thread.sleep(500);
}
}
} catch
(FileUploadException e) {
logger.error("上传文件时发生错误:"+e.getMessage());
e.printStackTrace();
uploadExceptionHandle(request,"上传文件时发生错误:"+e.getMessage());
} catch (Exception
e) {
//
TODO Auto-generated catch block
logger.error("保存上传文件时发生错误:"+e.getMessage());
e.printStackTrace();
uploadExceptionHandle(request,"保存上传文件时发生错误:"+e.getMessage());
}
if
(forwardURL.length()==0){
forwardURL=DEFAULT_UPLOAD_FAILURE_URL;
}
request.getRequestDispatcher(forwardURL).forward(request,response);
}
{BackGroundService中的responseFileUploadStatusPoll方法用于处理对文件上传状态的轮询请求}:
/**
* 回应上传状态查询
* @param request
* @param response
* @throws IOException
*/
private void responseFileUploadStatusPoll(HttpServletRequest
request,HttpServletResponse response)
throws IOException{
response.setContentType("text/xml");
response.setCharacterEncoding("UTF-8");
response.setHeader("Cache-Control", "no-cache");
logger.debug("发送上传状态回应");
response.getWriter().write(XmlUnSerializer.serializeBean(
request.getSession().getAttribute(UPLOAD_STATUS)));
}
{BackGroundService中的processCancelFileUpload方法用于处理取消文件上传的请求}:
/**
* 处理取消文件上传
* @param request
* @param response
* @throws IOException
*/
private void processCancelFileUpload(HttpServletRequest
request,HttpServletResponse response)
throws IOException{
FileUploadStatus
fUploadStatus=(FileUploadStatus)request.getSession().getAttribute(UPLOAD_STATUS);
fUploadStatus.setCancel(true);
request.getSession().setAttribute(UPLOAD_STATUS,
fUploadStatus);
responseFileUploadStatusPoll(request,response);
}
Web客户端代码:
web客户端使用了基于Prototype的AjaxWrapper类和XMLDomForAjax类,前者实现了对Ajax.Request功能的封装,而后者实现了对来自服务器的XML Response的反序列化(反序列化为JavaScript对象)。
为了避免在AjaxWrapper的回调方法中发生this被重写的问题,我使用了ClassUtils类给任何类的每个方法注册一个对类对象自身引用,详见《解开JavaScript生命的达芬奇密码》和《Prototype.AjaxRequest的调用堆栈重写问题》:
{ClassUtils类代码}:
{将XML反序列化为JavaScript对象的XMLDomForAjax类代码}:
var XMLDomForAjax=Class.create();
XMLDomForAjax.prototype={
isDebug:false,
//dom节点类型常量
ELEMENT_NODE:1,
ATTRIBUTE_NODE:2,
TEXT_NODE:3,
CDATA_SECTION_NODE:4,
ENTITY_REFERENCE_NODE:5,
ENTITY_NODE:6,
PROCESSING_INSTRUCTION_NODE:7,
COMMENT_NODE:8,
DOCUMENT_NODE:9,
DOCUMENT_TYPE_NODE:10,
DOCUMENT_FRAGMENT_NODE:11,
NOTATION_NODE:12,
initialize:function(isDebug){
new
ClassUtils().registerFuncSelfLink(this);
this.isDebug=isDebug;
},
/**
* 建立跨平台的dom解析器
* @param xml
xml字符串
* @return
dom解析器
*/
createDomParser:function(xml){
//
code for IE
if
(window.ActiveXObject){
var
doc=new ActiveXObject("Microsoft.XMLDOM");
doc.async="false";
doc.loadXML(xml);
}
//
code for Mozilla, Firefox, Opera, etc.
else{
var
parser=new DOMParser();
var
doc=parser.parseFromString(xml,"text/xml");
}
return
doc;
},
/**
*
反向序列化xml到javascript Bean
* @param xml
xml字符串
* @return
javascript Bean
*/
deserializedBeanFromXML:function
(xml){
var
funcHolder=arguments.callee.$;
var
doc=funcHolder.createDomParser(xml);
//
documentElement总表示文档的root
var
objDomTree=doc.documentElement;
var
obj=new Object();
for
(var i=0; i
var
node=objDomTree.childNodes[i];
//取出其中的field元素进行处理
if
((node.nodeType==funcHolder.ELEMENT_NODE) && (node.tagName
== 'field')) {
var
nodeText=funcHolder.getNodeText(node);
if
(funcHolder.isDebug){
alert(node.getAttribute('name')+'
type:'+node.getAttribute('type')+' text:'+nodeText);
}
var
objFieldValue=null;
//如果为列表
if
(node.getAttribute('type')=='java.util.List'){
if
(objFieldValue && typeof(objFieldValue)=='Array'){
if
(nodeText.length>0){
objFieldValue[objFieldValue.length]=nodeText;
}
}
else{
objFieldValue=new
Array();
}
}
else
if (node.getAttribute('type')=='long'
||
node.getAttribute('type')=='java.lang.Long'
||
node.getAttribute('type')=='int'
||
node.getAttribute('type')=='java.lang.Integer'){
objFieldValue=parseInt(nodeText);
}
else
if (node.getAttribute('type')=='double'
||
node.getAttribute('type')=='float'
||
node.getAttribute('type')=='java.lang.Double'
||
node.getAttribute('type')=='java.lang.Float'){
objFieldValue=parseFloat(nodeText);
}
else
if (node.getAttribute('type')=='java.lang.String'){
objFieldValue=nodeText;
}
else{
objFieldValue=nodeText;
}
//赋值给对象
obj[node.getAttribute('name')]=objFieldValue;
if
(funcHolder.isDebug){
alert(eval('obj.'+node.getAttribute('name')));
}
}
else
if (node.nodeType == funcHolder.TEXT_NODE){
if
(funcHolder.isDebug){
//alert('TEXT_NODE');
}
}
else
if (node.nodeType == funcHolder.CDATA_SECTION_NODE){
if
(funcHolder.isDebug){
//alert('CDATA_SECTION_NODE');
}
}
}
return
obj;
},
/**
* 获得dom节点的text
*/
getNodeText:function
(node) {
var
funcHolder=arguments.callee.$;
//
is this a text or CDATA node?
if (node.nodeType ==
funcHolder.TEXT_NODE || node.nodeType ==
funcHolder.CDATA_SECTION_NODE) {
return
node.data;
}
var
i;
var
returnValue = [];
for
(i = 0; i < node.childNodes.length; i++) {
//采用递归算法
returnValue.push(funcHolder.getNodeText(node.childNodes[i]));
}
return
returnValue.join('');
}
}
{AjaxWrapper类的主要方法putRequest和callBackHandler}:
/**{页面中主要的JavaScript方法:refreshUploadStatus和startProcess/cancelProcess}:
//刷新上传状态
function refreshUploadStatus(){
var ajaxW = new
AjaxWrapper(false);
ajaxW.putRequest(
'./uploadStatus.action',
'uploadStatus=',
function(responseText){
var
deserialor=new XMLDomForAjax(false);
var
uploadInfo=deserialor.deserializedBeanFromXML(responseText);
var
progressPercent = Math.ceil(
(uploadInfo.readTotalSize)
/ uploadInfo.uploadTotalSize * 100);
$('progressBarText').innerHTML
= ' 上传处理进度: '+progressPercent+'% ['+
(uploadInfo.readTotalSize)+'/'+uploadInfo.uploadTotalSize
+ ' bytes]'+
'
正在处理第'+uploadInfo.currentUploadFileNum+'个文件'+
'
耗时: '+(uploadInfo.processRunningTime-uploadInfo.processStartTime)+'
ms';
$('progressStatusText').innerHTML='
反馈状态: '+uploadInfo.status;
$('totalProgressBarBoxContent').style.width
= parseInt(progressPercent * 3.5) + 'px';
}
);
}
//上传处理
function startProgress(){
Element.show('progressBar');
$('progressBarText').innerHTML = ' 上传处理进度:
0%';
$('progressStatusText').innerHTML='
反馈状态:';
$('uploadButton').disabled = true;
var periodicalExe=new
PeriodicalExecuter(refreshUploadStatus,2);
return true;
}
//取消上传处理
function cancelProgress(){
$('cancelUploadButton').disabled
= true;
var ajaxW = new
AjaxWrapper(false);
ajaxW.putRequest(
'./uploadStatus.action',
'cancelUpload=true',
//因为form的提交,这可能不会执行
function(responseText){
var
deserialor=new XMLDomForAjax(false);
var
uploadInfo=deserialor.deserializedBeanFromXML(responseText);
$('progressStatusText').innerHTML='
反馈状态: '+uploadInfo.status;
if
(msgInfo.cancel=='true'){
alert('删除成功!');
window.location.reload();
};
}
);
}
运行界面:
源代码下载:
AjaxUpload.zip
相关链接:
AJAX Upload
progress monitor for Commons-FileUpload Example
Apache Common
FileUpload组件
Prototype官方网站
IBM的AJAX
SlideShow应用
解开JavaScript生命的达芬奇密码
Prototype.AjaxRequest的调用堆栈重写问题