You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

StreamingResponseBody提前返回致客户端获空ZIP文件问题求助

问题解决:StreamingResponseBody生成ZIP客户端立即收到空文件

核心问题分析

你的代码出现空ZIP的关键原因有以下几点:

  • ZipOutputStream未正确关闭/刷新:ZIP格式需要在写入所有文件后完成目录结构写入,未关闭流会导致客户端收到不完整的ZIP数据,表现为空文件。
  • 异常处理混乱:catch块中变量名混用(exp和e),且仅打印栈轨迹,容易吞掉流操作异常导致写入中断。
  • 异步任务处理逻辑冗余:先批量join再遍历get,既浪费时间又可能导致内存溢出(所有文件内容加载到内存)。
  • 方法参数不匹配:downloadDocument方法定义未接收i参数,但调用时传入了i++,存在逻辑错误。

修复方案及代码修改

1. 修复接口核心逻辑

@PostMapping(value = "/all", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<StreamingResponseBody> documentDownloadAll(@RequestBody MetadataParams metadataParams) {
    try {
        long start = System.currentTimeMillis();
        List<File> documentList = // 此处读取文件列表;
        StreamingResponseBody responseBody = outputStream -> {
            // 使用try-with-resources自动关闭ZipOutputStream
            try (ZipOutputStream zos = new ZipOutputStream(outputStream)) {
                List<CompletableFuture<Map<String, byte[]>>> results = new ArrayList<>();
                int i = 0;
                for (File document : documentList) {
                    CompletableFuture<Map<String, byte[]>> completableFuture = fileNetService.downloadDocumentAsyn(document, i++);
                    if (completableFuture != null) {
                        results.add(completableFuture);
                    }
                }

                // 等待所有异步任务完成,替代冗余的forEach join
                CompletableFuture.allOf(results.toArray(new CompletableFuture[0])).join();

                log.info("开始写入ZIP内容");
                for (CompletableFuturece(en可比性	   �(Images Sol生 vett使用 api的 MIN[不要,直接遍历
                for (CompletableFuture<Map<String, byte[]>> future : results) {
                    if (future == null || future.get() == null) {
                        continue;
                    }
                    Map<String, byte[]> fileMap = future.get();
                    for (Map.Entry<String, byte[]> entry : fileMap.entrySet()) {
                        String fileName = entry.getKey();
                        byte[] fileContent = entry.getValue();
                        try {
                            log.info("写入文件: {}", fileName);
                            zos.putNextEntry(new ZipEntry(fileName));
                            zos.write(fileContent);
                            zos.closeEntry();
                        } catch (IOException e) {
                            log.error("写入文件{}失败", fileName, e);
                        }
                    }
                }
                // 刷新流,确保所有数据写入输出流
                zos.flush();
                log.info("ZIP写入完成");
            } catch (Exception e) {
                log.error("生成ZIP失败", e);
                throw new RuntimeException("生成ZIP文件出错", e);
            }
        };

        log.info("接口初始化耗时: {}ms", (System.currentTimeMillis() - start));
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=download_all.zip")
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(responseBody);
    } catch (Exception exception) {
        log.error("接口处理失败", exception);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(outputStream -> outputStream.write("Error while downloading ZIP file".getBytes(StandardCharsets.UTF_8)));
    }
}

2. 修复异步下载方法

@Async("taskExecutorDoc")
public CompletableFuture<Map<String, byte[]>> downloadDocumentAsyn(File path, int i) { // 补充i参数
    try {
        log.info("开始下载文件: {},序号: {}", path.getAbsolutePath(), i);
        byte[] data = Files.readAllBytes(path.toAbsolutePath());
        Map<String, byte[]> fileMap = new HashMap<>();
        fileMap.put(i + "_" + path.getFileName(), data);
        return CompletableFuture.completedFuture(fileMap);
    } catch (Exception e) {
        log.error("下载文件{}失败", path.getAbsolutePath(), e);
        return CompletableFuture.failedFuture(e); // 返回失败的Future,便于上层处理
    }
}

关键修改点说明

  • 自动关闭ZipOutputStream:用try-with-resources确保流最终被关闭,写入ZIP的中央目录信息,客户端能正确解析ZIP。
  • 统一异常处理:替换e.printStackTrace()为日志记录,避免异常吞掉,同时抛出异常让Spring处理错误响应。
  • 修正响应头文件名:将.bin改为.zip,让客户端识别正确的文件类型。
  • 优化异步任务等待逻辑:用CompletableFuture.allOf()替代批量join,代码更简洁高效。
  • 修复异步方法参数:补充i参数,解决调用时参数不匹配的问题。

额外优化建议

如果处理大文件或大量文件,建议避免将文件内容全部加载到内存,改为在异步任务中直接写入ZipOutputStream(需注意线程安全),示例如下:

@Async("taskExecutorDoc")
public CompletableFuture<Void> downloadAndWriteToZip(File path, int i, ZipOutputStream zos) {
    return CompletableFuture.runAsync(() -> {
        try (InputStream is = Files.newInputStream(path.toPath())) {
            zos.putNextEntry(new ZipEntry(i + "_" + path.getFileName()));
            is.transferTo(zos);
            zos.closeEntry();
        } catch (IOException e) {
            log.error("写入文件{}失败", path.getAbsolutePath(), e);
            throw new RuntimeException(e);
        }
    });
}

内容的提问来源于stack exchange,提问作者Dayananda

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 01:57:05