服务热线:13616026886

技术文档 欢迎使用技术文档,我们为你提供从新手到专业开发者的所有资源,你也可以通过它日益精进

位置:首页 > 技术文档 > 专题栏目 > WEB2.0新技术 > 查看文档

掌控上传进度的ajax upload

  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的实现:

  image

  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方法用于处理文件上传请求}:

        /**
         * 处理文件上传
         * @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);
        }

 

  {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类给任何类的每个方法注册一个对类对象自身引用

  {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}:

//刷新上传状态
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);
 &nbs