阅读 365

Java实现批量下载多文件(夹)压缩包(zip)续

问题现状

在Java实现批量下载多文件(夹)压缩包(zip)篇幅中通过在服务器上创建临时文件,借助hutoolZipUtil将文件(夹)压缩写入至responseOutputStream,实现了多文件(夹)的压缩包下载。其大致流程图可大致描述为:

POST请求下载文件

经过分析和验证上述方式实现的批量下载存在着下列问题

  • 1.文件非常大的情形下,步骤1.2. 4将文件先下载到服务器带来了额外的耗时操作,对于用户来说下载文件只需要将文件从文件系统直接写入响应即可。

  • 2.由于请求类型为POST,所以浏览器不能自动下载文件,步骤5即使将流已写入响应,但是浏览器并不能打开下载页面,需要前端接收到所有Blob才能打开下载,用户体验极差,易给用户造成批量下载没反应的错觉。

是否存在一种方案,可以将批量下载接口转为GET请求,且可以将文件(夹)直接写入到response的OutputStream?

解决思路

1.首先由于批量下载接口batchDownloadFile的参数类型为List<DownloadFileParam>为复杂参数,故无法直接将POST请求修改为GET;这时候该怎么办呢?

架构思维中,比较常用的一种思路便是分层架构!我门可以将批量下载接口拆为两个接口

通过POST方式保存下载参数List<DownloadFileParam>Redis,并返回Redis中该下载参数对应唯一标示key的接口getBatchDownloadKey如下

@PostMapping(value = "/getBatchDownloadKey") public String getBatchDownloadKey(@RequestBody List<DownloadFileParam> params)... 复制代码

根据返回下载参数唯一标示Key进行批量下载的GET接口batchDownloadFile接口,定义如下

 @GetMapping(value = "/batchDownloadFile", produces = "application/octet-stream) public void batchDownloadFile(@RequestParam("downLoadKey") String downLoadKey)      复制代码

2.Java提供了类ZipArchiveOutputStream允许我们可以直接将带有目录结构的文件压缩到OutputStream,其使用的伪代码如下
ZipArchiveOutputStream zous = new ZipArchiveOutputStream(response.getOutputStream()); //file为带有目录结构的文件比如:/文件夹/子文件夹/文件.txt ArchiveEntry entry = new ZipArchiveEntry(file); InputStream inputStream = file.getInputStream(); zous.putArchiveEntry(entry); try {   int len;   byte[] bytes = new byte[1024];   //inputStream为文件流   while ((len = inputStream.read(bytes)) != -1) {     zous.write(bytes, 0, len);   }     zous.closeArchiveEntry();   zous.flush();   } catch (Exception e) {     e.printStackTrace();   } finally {     IoUtil.close(inputStream);   } 复制代码

这样我们就可以避免将文件下载到服务器带来的性能消耗。

3.整个过程的流程图如下

GET请求批量下载 (1)

代码实现

保存下载参数请求getBatchDownloadKey

@PostMapping(value = "/getBatchDownloadKey") public String getBatchDownloadKey(@RequestBody List<DownloadFileParam> params) throws Exception {     try {         String key = IdGenerator.newShortId();         redisTemplate.opsForValue().set(key, JSONObject.toJSONString(params), 60, TimeUnit.SECONDS);         return key;     } catch (Exception e) {         logger.error("getBatchDownloadKey error params={}", params, e);         throw e;     } } 复制代码

根据Key下载文件的接口定义batchDownloadFile

@GetMapping(value = "/pass/batchDownloadFile", produces = "application/octet-stream;charset=UTF-8") public void batchDownloadFile(@RequestParam("downLoadKey") String downLoadKey,@RequestParam("token") String token) throws Exception {     try {         fileService.batchDownloadFile(downLoadKey, getRequest(), getResponse(),token);     } catch (Exception e) {         logger.error("batchDownloadFile error params={}", downLoadKey, e);         throw e;     } } 复制代码

fileService.batchDownloadFile

@Override     public void batchDownloadFile(String key, HttpServletRequest request, HttpServletResponse response,String token) throws Exception {         if (redisUtil.get(token) != null) {             UserSession userSession = JSONObject.parseObject(redisUtil.get(token).toString(), UserSession.class);             //如果存在session或者token是存在于project_token配置的值,通过认证             if (userSession != null) {                 Object result = redisTemplate.opsForValue().get(key);                 if (result == null) {                     throw new ParamInvalidException("无效的批量下载参数key");                 }                 List<DownloadFileParam> params = JSONArray.parseArray(result.toString(), DownloadFileParam.class);                 //创建虚拟文件夹                 String mockFileName = IdGenerator.newShortId();                 String tmpDir = "";                 FileUtil.mkdir(tmpDir);                 ZipArchiveOutputStream zous = null;                 try {                     //设置响应                     response.reset();                     response.setContentType("application/octet-stream");                     response.setHeader("Accept-Ranges", "bytes");                     String fileName = URLEncoder.encode(DateFormatUtil.formatDate(DateFormatUtil.yyyyMMdd, new Date()) + ".zip", "UTF-8").replaceAll("\\+", "%20");                     response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);                     response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");                     //参数组装                     zous = new ZipArchiveOutputStream(response.getOutputStream());                     zous.setUseZip64(Zip64Mode.AsNeeded);                     DownloadFileParam downloadFileParam = new DownloadFileParam();                     downloadFileParam.setFileName(mockFileName);                     downloadFileParam.setIsFolder(1);                     downloadFileParam.setChilds(params);                     //递归文件流添加zip                     downloadFileToServer(tmpDir, downloadFileParam, zous);                     zous.closeArchiveEntry();                 } finally {                     zous.close();                 }             } else {                 throw new ResultException("服务内部错误");             }         } else {             throw new ResultException("用户已下线,请重新登录");         }     } 复制代码

downloadFileToServer

private void downloadFileToServer(String tmpDir, DownloadFileParam downloadFileParam, ZipArchiveOutputStream zous) throws Exception {     List<DownloadFileParam> childs = downloadFileParam.getChilds();     if (EmptyUtils.isNotEmpty(childs)) {         final String finalPath = tmpDir;         childs.stream().forEach(dwp -> dwp.setFile(EmptyUtils.isNotEmpty(finalPath) ? finalPath + File.separator + dwp.getFileName() : dwp.getFileName()));         for (int i = 0; i < childs.size(); i++) {             DownloadFileParam param = childs.get(i);             if (param.getIsFolder() == 0) {                 FileInfo fileInfo = fileInfoDao.findById(param.getFileId()).orElseThrow(() -> new DataNotFoundException("文件不存在或已被删除!"));                 List<GridFsResource> gridFSFileList = fileChunkDao.findAll(fileInfo.getFileMd5());                 ArchiveEntry entry = new ZipArchiveEntry(param.getFile());                 zous.putArchiveEntry(entry);                 if (gridFSFileList != null && gridFSFileList.size() > 0) {                     try {                         for (GridFsResource gridFSFile : gridFSFileList) {                             InputStream inputStream = gridFSFile.getInputStream();                             try {                                 int len;                                 byte[] bytes = new byte[1024];                                 while ((len = inputStream.read(bytes)) != -1) {                                     zous.write(bytes, 0, len);                                 }                             } finally {                                 IoUtil.close(inputStream);                             }                         }                         zous.closeArchiveEntry();                         zous.flush();                     } catch (Exception e) {                         e.printStackTrace();                     }                 }             }             //递归下载文件到压缩流             downloadFileToServer(tmpDir, param, zous);         }     } } 复制代码

方案总结

一般情况下下载接口最好用GET方式,浏览器会自动开始下载,除此之外,接口参数与下载接口参数间通过添加中间层解藕帮我们解决了POST下载转化为GET下载方式的问题,分层的架构思想是软件架构最常用的一种方式,再解决工作实际问题的过程中,我们要善于变通采用该方式。


作者:被迫成为奋斗b
链接:https://juejin.cn/post/7020398932630962207


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐