2006年底google了一下ajax upload实现,结果没有发现很完整的java实现。硕果仅存的就是telio公司的pierre-alexandre发表的《ajax upload progress monitor for commons-fileupload example》一文。虽然文中完成upload工作的是common-fileupload组件,但在其代码中没有1.2版本所提供的listener功能,这就对检测文件上传情况造成了困难...
动机:
2006年底google了一下ajax upload实现,结果没有发现很完整的java实现。硕果仅存的就是telio公司的pierre-alexandre发表的《ajax upload progress monitor for commons-fileupload example》文中提供的ajax-upload-1.0.war。
虽然上文中完成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+servlet的example
所以出于研究prototype之目的,本人经过仔细思考,尝试实现了一个prototype+servlet的简单example。其工作流程很简单:
- 1.在form提交上传文件field的同时,使用ajax周期性地从servlet轮询上传状态信息;
- 2.然后,根据此信息更新进度条和相关文字,及时反映文件传输状态;
- 3.如果用户取消上传操作,则进行相应的现场清理工作:删除已经上传的文件,在form提交页面中显示相关信息;
- 4.如果上传完毕,在form提交页面中显示已经上传的文件内容(或链接),也可以与一些ajax slideshow应用结合在一起。
源代码下载:
ajaxupload.zip
服务器端代码:
bean序列化/反序列化工作:xmlunserializer这个类虽然不能够通吃任何模样的bean,但应付一般的bean、具有collection类型属性的bean和bean list来讲还是够用的。
{xmlunserializer类的核心方法serializebean和serializebeanlist}:
| /** * 将bean系列化为utf-8编码的xml * @param beanobj * @return * @throws ioexception */ public static string serializebean(object beanobj) throws ioexception{ … } /** * 将bean列表序列化为utf-8编码的xml * @param beanobj * @return * @throws ioexception */ public static string serializebeanlist(object beanlistobj) throws ioexception{ … } |
文件上传状态bean:使用fileuploadstatus这个类记录文件上传状态,并将其作为服务器端与web客户端之间通信的媒介物:通过对这个类对象进行xml序列化作为服务器回应发送给web客户端,web客户端使用javascript对其进行反序列化处理获得javascript版本的文件上传状态对象。
{fileuploadstatus的属性}:
| //上传总量 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方法用于处理文件上传请求}:
/** //取消上传 {backgroundservice中的responsefileuploadstatuspoll方法用于处理对文件上传状态的轮询请求}:
* 处理文件上传
* @param request
* @param response
* @throws ioexception
* @throws servletexception
*/
private void processfileupload(httpservletrequest request, httpservletresponse response) throws servletexception, ioexception{
diskfileitemfactory factory = new diskfileitemfactory();
//设置内存阀值,超过后写入临时文件
factory.setsizethreshold(10240000);
//设置临时文件存储位置
factory.setrepository(new file(request.getrealpath("/upload/temp")));
servletfileupload upload = new servletfileupload(factory);
//设置单个文件的最大上传size
upload.setfilesizemax(10240000);
//设置整个request的最大size
upload.setsizemax(10240000);
upload.setprogresslistener(new fileuploadlistener(request.getsession()));
//保存初始化后的fileuploadstatus bean
storefileuploadstatusbean(request.getsession(),initfileuploadstatusbean(request));
string forwardurl="";
try {
list items = upload.parserequest(request);
//获得返回url
for(int i=0;i<items.size();i++){
fileitem item=(fileitem)items.get(i);
if (item.isformfield()){
logger.debug("form field["+item.getfieldname()+"]="+item.getstring());
forwardurl=item.getstring();
break;
}
}
//处理文件上传
for(int i=0;i<items.size();i++){
fileitem item=(fileitem)items.get(i);
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);
}
/**
* 回应上传状态查询
* @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类给任何类的每个方法注册一个对类对象自身引用
{classutils类代码}:
| //类工具 var classutils=class.create(); classutils.prototype={ _classutilsname:'classutils', initialize:function(){ }, /** * 给类的每个方法注册一个对类对象的自我引用 * @param reference 对类对象的引用 */ registerfuncselflink:function(reference){ for (var n in reference) { var item = reference[n]; if (item instanceof function) item.$ = reference; } } } |
{将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<objdomtree.childnodes.length; 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}:
| /** * 以get的方式向server发送request * @param url * @param params * @param callbackfunction 发送成功后回调的函数或者函数名 */ putrequest:function(url,params,callbackfunction){ var funcholder=arguments.callee.$; var xmlhttp = new ajax.request(url, { method: 'get', parameters: params, requestheaders:['my-header-encoding','utf-8'], onfailure: function(){ alert('对不起,网络通讯失败,请重新刷新!'); }, onsuccess: function(transport){ }, oncomplete: function(transport){ funcholder.callbackhandler.apply(funcholder,[transport,callbackfunction]); } }); }, /** * 远程调用的回调处理 * @param transport xmlhttp的transport * @param callbackfunction 回调时call的方法,可以是函数也可以是函数名 */ callbackhandler:function(transport,callbackfunction){ var funcholder=arguments.callee.$; if(transport.status!=200){ alert("获得回应失败,请求状态:"+transport.status); } else{ funcholder.xml_source=transport.responsetext; if (funcholder.debug_flag) alert('call callback function'); if (typeof(callbackfunction)=='function'){ if (funcholder.debug_flag){ alert('invoke callbackfunc'); } callbackfunction(transport.responsetext); } else{ if (funcholder.debug_flag){ alert('evalfunc callbackfunc'); } new execute().evalfunc(callbackfunction,transport.responsetext); } if (funcholder.debug_flag) alert('end callback function'); } } |
{页面中主要的javascript方法:refreshuploadstatus和startprocess/cancelprocess}:
|
//刷新上传状态 $('progressbartext').innerhtml = ' 上传处理进度: '+progresspercent+'% ['+ 扫描关注微信公众号 |
闽公网安备 35060202000074号