-
需求:前端发出下载请求,当后端推送文件完毕后需要给前端一个下载完毕的信号
-
技术选型:springmvc js jsp
-
思路
-
下载完毕后返回一个视图(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时,方法处理完之后视图为空
-
对response设置响应头,使页面刷新
response.setHeader("refresh","1");//每秒刷新一次
此种方法写法不对,因为本意是只让页面刷新一次就可以,无需重复刷新,但除开写法的原因这样写也不会生效,因为下载文件需要对响应头进行修改(Content-Disposition),当下载完毕再次修改响应头时会修改失效(可能响应发出后,即推送文件流后,就无法再修改响应头了)
关于设置response响应头的更多写法:
-
下载方法返回类型为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(); } } }
此种方法有效,但美中不足的是在下载完成后跳转了页面,很不人性化。能否在下载请求正常完成后向前端传递信号呢?
-
基于上述方法(方法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<cookies.length;i++){ Cookie cookie = cookies[i]; if(cookie.getName().equals(targetName))){ %> return "success"; <% } } } %> } //此处省略delCookie()具体写法
对于上述jsp代码在实际运行中发现了一些问题:
(1)当重复查询cookie时,每次查询的结果与第一次的结果相同,即使在后台新增了cookie后也是如此,而当刷新jsp页面后就能获取到最新的cookie。这让笔者得出一些结论:脚本片段中的代码是不会重复执行多次的。这或许与jsp的执行原理有关。
(2)当刷新页面后获取到最新的cookie后随即执行delCookie()删除目标cookie,这一流程是可以是实现的。也正是上述(1)中的结论:脚本片段中的代码可以执行一次。
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();//页面刷新 } } <body> <input type="hidden" id="downloadStatus" value="${ downloadStatus }"/> </body>
此种方法依然无效,无法实时获取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向前端主动推送消息。
其他可参考文章: