今天,调整了一个旧项目的报表下载功能,原来文件是存储在服务器本地的,下载直接从本机获取就可以了,现在要改成从 FTP 服务器获取文件再返回给前台。
理论上,对代码稍微调整就可以了,实际上却踩了一个小坑,本文将整理 Java Web 应用文件下载的流程及注意点。
文件下载是一个老生常谈的功能了,基本原理是直接向响应流写数据,并设置响应类型为二进制流格式:
第二、三、四步对应的代码为:
常见的文件下载代码为:
@ResponseBody @RequestMapping(value = "/download") public void download(HttpServletRequest request, HttpServletResponse response,String reportId) { // TODO 根据 reportId 查询报表对应的文件名称 String fileName = "xxx日报表文件.xlsx"; //设置响应类型和附件头域 response.setCharacterEncoding("utf-8"); response.setContentType("application/octet-stream"); response.setHeader("content-disposition", "attachment;filename="+ fileName); //读取报表内容写入响应流对象 InputStream inputStream = null; try { OutputStream output = response.getOutputStream(); //检查文件是否存在 if(!isFileExist(fileName)){ logger.warn("文件/目录 {} 不存在", pathName); response.getWriter().println("报表文件不存在!"); return; } inputStream = new FileInputStream(new File(pathName)); int len = -1; byte[] bytes = new byte[2048]; // 向 Response 的响应流写入二进制数据 while ((len = inputStream.read(bytes)) != -1) { output .write(bytes, 0, len); } output.flush(); } catch (Exception e) { logger.error("下载文件异常",e); try { response.getWriter().println("下载文件异常!"); } catch (IOException ex) { ex.printStackTrace(); } }finally{ if(output != null){ try { output.close(); } catch (IOException e) { logger.error("下载文件关闭输出流异常",e); } } if(inputStream != null){ try { inputStream.close(); } catch (IOException e) { logger.error("下载文件关闭输入流异常",e); } } } } 复制代码
敲重点
,运行下载操作后,xlsx 类型的报表文件现在下载栏中,查看网络请求的响应头为:
设置响应类型和头域信息必须在 write
写入之前 ,否则附件就是不可读的。调整代码顺序,先写后设置响应头:
执行下载操作,发现 xlsx 类型的报表以 .zip 压缩包格式被下载,且内容不可读。
查看网络响应的头,可以看出,之后设置的头域没有生效:
为什么设置顺序不同,下载附件显示就不一样呢?
反复验证了十几次,发现 response.setHeader
在 write
操作之后时,头域设置是无效的。推测这是由 http 通信协议包组装顺序决定的, 因为 http 响应头域信息是在 body 之前组装的
。
最后,再总结下文件下载的要点:
../..
路径的任意文件下载风险;如果要接收带路径的 fileName
参数,必须校验 fileName
不能包含 ../
等特殊路径; 写设头域后写
,否则附件不可用; ServletResponse
的 OutputStream
对象传给 FTPClient
的 retrieveFile(filename, outputStream)
直接下载到输出流中。