记一次下载状态获取

  • 需求:前端发出下载请求,当后端推送文件完毕后需要给前端一个下载完毕的信号

  • 技术选型:springmvc js jsp

  • 思路

  1. 下载完毕后返回一个视图(ModelAndView),利用视图传递参数

    //此为初步构想代码
    protected ModelAndView download(HttpServletRequest req,HttpServletResponse resp,
                                   String attribute){
        ServletOutputStream outStream;
        HSSFWorkbook workbook;
        ModelAndView mav ;
        //RESULT_PAGE 专门展示后端返回值的jsp(/pages/result.jsp)
        //AmluConstants 常量类,存放各种参数
        //AmluConstants.REQUEST_RESULT_INFO 需要向request中写返回值时,key的名称
        //ResultInfoManager 返回信息组装类,根据异常类型返回相应提示语
        try{
            //工作簿对象生成
            workbook = getWorkbook(attribute);
            //抛出文件生成异常
            if(workbook==null){
    			request.setAttribute(AmluConstants.REQUEST_RESULT_INFO,
                             ResultInfoManager.getWorkbookErrorInfo(e));
                mav = new ModelAndView(RESULT_PAGE);
                return mav;
            }
            //文件推送
            resp.reset();
            resp.setContentType("application/msexcel;charset=utf-8");
            resp.setHeader("Content-Disposition","attachment;filename="+
                          URLEncoder.encode(fileName,"UTF-8")+".xls");
            outStream = response.getOutputStream();
            workbook.write(outStream);
            outStream.flush();
            //完成状态推送
            mav = new ModelAndView();
            mav.addObject("message","success");
            return mav;
        }catch(Exception e){
            logger.error(e);
    		req.setAttribute(AmluConstants.REQUEST_RESULT_INFO,
                                 ResultInfoManager.getDownloadErrorInfo(e));
            mav = new ModelAndView(RESULT_PAGE);
            return mav;
        }finally{
            if(outStream!=null){
                outStream.close();
            }
        }
    }
    

    理想情况下,若有异常抛出则跳转至RESULT_PAGE页面并显示错误信息,若正常状态则可以传递给前端信息[message:success] , 但通过Debug可发现上述写法会导致返回的视图为null,原因如下:

    controller方法参数中带HttpServletResponse response时,方法处理完之后视图为空

    参考文档:springmvc 拦截器中ModelAndView为null

  2. 对response设置响应头,使页面刷新

    response.setHeader("refresh","1");//每秒刷新一次
    

    此种方法写法不对,因为本意是只让页面刷新一次就可以,无需重复刷新,但除开写法的原因这样写也不会生效,因为下载文件需要对响应头进行修改(Content-Disposition),当下载完毕再次修改响应头时会修改失效(可能响应发出后,即推送文件流后,就无法再修改响应头了)

    关于设置response响应头的更多写法:

    参考文档:response里的setHeader用法

  3. 下载方法返回类型为void,向request中写值,跳转到特定页面读值显示信息

    跳转至别的页面有两种方式,重定向和请求转发

    重定向会告诉客户端目标网址是什么,并返回响应,客户端接收到响应后会再次发送请求去刚刚得到的目标网址,所以这中间存在两次request请求,如果向request中写值的话,重定向后request内的值会丢失。

    protected void download(HttpServletRequest req,HttpServletResponse resp,
                                   String attribute){
        ServletOutputStream outStream;
        HSSFWorkbook workbook;
        try{
            //工作簿对象生成
            workbook = getWorkbook(attribute);
            //抛出文件生成异常
            if(workbook==null){
    			throw new NullArgumentException("未获取到对应模板文件");
            }
            //文件推送
            resp.reset();
            resp.setContentType("application/msexcel;charset=utf-8");
            resp.setHeader("Content-Disposition","attachment;filename="+
                          URLEncoder.encode(fileName,"UTF-8")+".xls");
            outStream = response.getOutputStream();
            workbook.write(outStream);
            outStream.flush();
            //完成状态推送
            req.setAttribute(AmluConstants.REQUEST_RESULT_INFO,
                                 "下载成功");
            req.getRequestDispatcher(RESULT_PAGE).forward(req,resp);
        }catch(Exception e){
            logger.error(e);
    		req.setAttribute(AmluConstants.REQUEST_RESULT_INFO,
                                 ResultInfoManager.getDownloadErrorInfo(e));
            //采用请求转发进行错误页面跳转
            req.getRequestDispatcher(RESULT_PAGE).forward(req,resp);
        }finally{
            if(outStream!=null){
                outStream.close();
            }
        }
    }
    

    此种方法有效,但美中不足的是在下载完成后跳转了页面,很不人性化。能否在下载请求正常完成后向前端传递信号呢?

  4. 基于上述方法(方法3)的下载完成后的前端传值尝试

    4.1 尝试读取cookie

    后端代码只需修改 //完成状态推送 后的代码

    //更新完后,设定cookie,用于页面判断更新完成后的标志
    Cookie status = new Cookie("downloadStatus","success");
    status.setMaxAge(600);
    //添加cookie操作必须在写出文件前,如果写在后面,随着数据量增大时cookie无法写入
    response.addCookie(status);
    
    var timer1 = setInterval(refreshPage,1500);
    function refeshPage(){
     if(getCookie("downloadStatus")=="success"){
        clearInterval(timer1);//每隔一秒的判断操作停止
        delCookie("updateStatus");//删除cookie
        windows.location.reload();//页面刷新
     }
    }
    function getCookie(){
    <%
     String targetName="downloadStatus";
     request.getCookies();
     Cookie[] cookies=request.getCookies();
     if(cookies!=null){
     	for(int i=0;i
     			return "success";
     			<%
              }
          }
      }
    %>
    }
    //此处省略delCookie()具体写法
    

    对于上述jsp代码在实际运行中发现了一些问题:

    ​ (1)当重复查询cookie时,每次查询的结果与第一次的结果相同,即使在后台新增了cookie后也是如此,而当刷新jsp页面后就能获取到最新的cookie。这让笔者得出一些结论:脚本片段中的代码是不会重复执行多次的。这或许与jsp的执行原理有关。

    ​ (2)当刷新页面后获取到最新的cookie后随即执行delCookie()删除目标cookie,这一流程是可以是实现的。也正是上述(1)中的结论:脚本片段中的代码可以执行一次。

    类似案例:为什么这个JSP程序只能执行一次

    4.2 尝试EL表达式(${ xxx })取值

    后端代码只需修改 //完成状态推送 后的代码

    request.setAttribute("downloadStatus","success");
    
    var timer1 = setInterval(refreshPage,1500);
    function refreshPage(){
    	var downloadStatus=$("#downloadStatus").val();
    	if(downloadStatus=="success"){
    		<%request.removeAttribute("downloadStatus")%>
    		windows.location.reload();//页面刷新
    	}
    }
    
    
    	
    
    

    此种方法依然无效,无法实时获取request中的实时参数,原因未知。【挖坑】

    4.3 尝试循环调用异步请求查询实时参数

    //这次将参数写在了session中
    HttpSession session = req.getSession();
    session.setAttribute("downloadStatus_37","success");
    //参数key详细化,以便后续此种方法大量复用
    
    //控制层增加方法
    @RequestMapping(params="method=queryStatus")
    @ResponseBody
    public Object queryStatus(String name,HttpServletRequest req){
        return req.getSession().getAttribute(name);
    }
    
    var timer1;
    function queryStatus(){
    	var url = "${pageContext.request.contextPath}/budgtBusDiff.htm?method=queryStatus";
    	$.ajax({
    		type:"get",
    		url:url,
    		data: "name=downloadStatus_37",
    		dataType:"json",
    		cache:false,
    		success:function(msg){
    			if(msg=='success'){
    				//销毁对应的参数
    				<%request.getSession().removeAttribute("downloadStatus_37")%>
    				window.location.reload();//页面刷新,恢复初始下载状态
    			}
    		},
    		error:function(msg){
    				console.log("ajax call failed:queryStatus()");
    				console.log(msg);
    		},
    		complete:function(msg){}
    	})
    }
    function download_onclick(){
    	DOWNLAODFORM.submit();
    	$('#background').show();//生成背景遮罩
    	timer1=setInterval(queryStatus,3000);//提交下载请求后再开始查询状态
    }
    

    此种方法可行。

    可参考的文档:前端显示后端处理进度的简单实现

  • 其他想法:

    对于“下载进度感知”这一需求,是否可以有更多的处理方式?

    我们知道浏览器文件下载的进度条大多是根据响应头的content-length字段进行下载进度展示的,网上对应也有很多插件组件可供选择,但对于实时查表然后再生成文件并推送的下载需求,此种进度展示方法并不合适,因为从用户角度来说,等待查询结果和等待文件推送完毕都是“等待”的一部分,并无可区分的差异。

    如果要感知sql语句的查询进度,该怎么做呢?笔者有俩种思路:

    ​ (1)先count一下数据量,由特定算法函数给出具体的查询时间,将剩余查询时间传递给前端,但此种方法需要知道总的数据量,如果需要多次查询,数据总量不好给出;

    ​ (2)预先在程序中埋点,当执行完某一部分的代码后就更新“下载进度参数”,此种方法较为繁琐,但如果能借助框架的支持就会简单很多。

    对于消息的推送,也可以采用WebSocket向前端主动推送消息。

    其他可参考文章:

    websocket进阶下载进度监控_哔哩哔哩_

    下载文件时显示动态的进度条(前端easyUI,后台java)

你可能感兴趣的:(java,ajax,jsp)