其他分享
首页 > 其他分享> > 记一次下载状态获取

记一次下载状态获取

作者:互联网

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

- 技术选型:springmvc js jsp

- 思路

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

     ```java
     //此为初步构想代码
     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](https://www.oschina.net/question/1406135_248974?sort=time)

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

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

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

     > 关于设置response响应头的更多写法:
     >
     > [参考文档:response里的setHeader用法](https://blog.csdn.net/zhangqin18710923356/article/details/78084657)

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

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

     ```java
     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)的下载完成后的前端传值尝试

     1. 尝试读取cookie 
     
     *后端代码只需修改 //完成状态推送 后的代码*
     
     ```java
     //更新完后,设定cookie,用于页面判断更新完成后的标志
     Cookie status = new Cookie("downloadStatus","success");
     status.setMaxAge(600);
     //添加cookie操作必须在写出文件前,如果写在后面,随着数据量增大时cookie无法写入
     response.addCookie(status);
     ```
     
     
     
       ```jsp
     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)中的结论:脚本片段中的代码可以执行一次。
     
     > [类似案例:为什么这个JSP程序只能执行一次](https://bbs.csdn.net/topics/30069552)
     
     2. 尝试EL表达式(${ xxx })取值
     
     *后端代码只需修改 //完成状态推送 后的代码*
     
     ```java
     request.setAttribute("downloadStatus","success");
     ```
     
     
     
     ```jsp
     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中的实时参数,原因未知。【挖坑】
     
     3. 尝试循环调用异步请求查询实时参数
     
     ```java
     //这次将参数写在了session中
     HttpSession session = req.getSession();
     session.setAttribute("downloadStatus_37","success");
     //参数key详细化,以便后续此种方法大量复用
     ```
     
     ```java
     //控制层增加方法
     @RequestMapping(params="method=queryStatus")
     @ResponseBody
     public Object queryStatus(String name,HttpServletRequest req){
         return req.getSession().getAttribute(name);
     }
     ```
     
     ```jsp
     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);//提交下载请求后再开始查询状态
     }
     ```
     
     此种方法可行。
     
     > [可参考的文档:前端显示后端处理进度的简单实现](https://blog.csdn.net/shijiujiu33/article/details/85228033?locationNum=14 fps=1)

- 其他想法:

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

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

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

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

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

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

  > 其他可参考文章:
  >
  > [websocket进阶下载进度监控_哔哩哔哩_](https://www.bilibili.com/video/av48635648)
  >
  > [下载文件时显示动态的进度条(前端easyUI,后台java)](https://www.jianshu.com/p/28fba5470844)
 

标签:状态,req,outStream,获取,cookie,RESULT,推送,下载
来源: https://blog.csdn.net/Ka__ze/article/details/118763273