• 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下载方式的问题,分层的架构思想是软件架构最常用的一种方式,再解决工作实际问题的过程中,我们要善于变通采用该方式。

    来源:https://juejin.cn/post/7020398932630962207
  • 相关阅读:
    Jmeter错误解析-明明写对了,确报404,原因是接口里多了个空格
    Jmeter+ant+svn+jenkins一键部署(四)--机器人发报告
    Django
    Linux下压缩解压文件夹+下载+删除命令
    VUE-安装
    Hadoop核心-MapReduce
    Pulsar 社区周报|2021-06-14~2021-06-20
    ApacheCon Asia 2021: Apache Pulsar 技术议题一览
    Pulsar 社区周报|2021-06-07~2021-06-13
    直播回顾|TGIP-CN 032:Apache Pulsar 快速上手实战
  • 原文地址:https://www.cnblogs.com/konglxblog/p/16456461.html
Copyright © 2020-2023  润新知