记一次下载状态获取
作者:互联网
- 需求:前端发出下载请求,当后端推送文件完毕后需要给前端一个下载完毕的信号
- 技术选型: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