Java 8大文件下载REST API超时问题解决方案咨询
大体积报表下载超时优化方案
针对会话时长限制30秒的场景,目前同步全量传输大报表的方式必然会触发超时,以下是具体优化方案:
1. 流式传输(最直接的优化)
核心思路是边生成数据边传输,避免一次性加载全量数据到内存,同时缩短数据传输的等待时间。
服务端代码修改
将一次性获取全量数据改为流式获取(比如用JPA的Stream查询、分批查询),同时流式写入响应,可配合Gzip压缩减少传输体积:
@GetMapping("/download") public void downloadReport(HttpServletResponse response) throws IOException { // 改为流式获取数据,避免内存溢出+减少等待时间 try (Stream<Person> peopleStream = getPeopleStream()) { response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName); response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); // 启用Gzip压缩,大幅减少传输数据量 response.addHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); try (GZIPOutputStream gzipOut = new GZIPOutputStream(response.getOutputStream())) { peopleStream.forEach(person -> { try { byte[] line = person.toString().concat("\n").getBytes(StandardCharsets.UTF_8); gzipOut.write(line); gzipOut.flush(); // 实时发送数据,避免缓存堆积 } catch (IOException e) { throw new RuntimeException("写入响应失败", e); } }); } } }
注意:
getPeopleStream()需要实现为流式数据查询,比如JPA中使用entityManager.createQuery("select p from Person p", Person.class).getResultStream(),而非一次性返回List。
客户端代码修改
放弃一次性接收全量字节数组,改为流式读取响应并直接转发,避免内存占用过高:
@GetMapping("/download") public void downloadReport(@RequestParam MultiValueMap<String, String> downloadRequest, HttpServletResponse response) throws Exception { RestClient restClient = RestClient.create(); restClient.get() .uri(URI) .queryParams(downloadRequest) .retrieve() .onStatus(HttpStatus::isError, (req, resp) -> { throw new Exception("报表下载失败"); }) .body((inputStream, context) -> { // 透传服务端的响应头 String disposition = context.getResponseHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION); response.addHeader(HttpHeaders.CONTENT_DISPOSITION, disposition); response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); // 透传压缩头(如果服务端启用了Gzip) String encoding = context.getResponseHeaders().getFirst(HttpHeaders.CONTENT_ENCODING); if ("gzip".equals(encoding)) { response.addHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); } // 边读边写,降低内存占用+缩短等待 StreamUtils.copy(inputStream, response.getOutputStream()); response.flushBuffer(); return null; }); }
2. 异步生成+轮询下载(超大数据量场景)
如果报表生成本身就超过30秒,流式传输也无法满足会话限制,可采用异步生成+客户端轮询的方式:
服务端新增接口
// 1. 提交报表生成任务,返回任务ID @PostMapping("/generate-report") public ResponseEntity<String> submitReportTask() { String taskId = UUID.randomUUID().toString(); reportService.generateReportAsync(taskId); return ResponseEntity.ok(taskId); } // 2. 查询任务状态 @GetMapping("/report-status/{taskId}") public ResponseEntity<ReportStatus> getTaskStatus(@PathVariable String taskId) { ReportStatus status = reportService.getTaskStatus(taskId); return ResponseEntity.ok(status); } // 3. 下载已生成的报表 @GetMapping("/download-generated/{taskId}") public void downloadGeneratedReport(@PathVariable String taskId, HttpServletResponse response) throws IOException { File reportFile = reportService.getGeneratedReport(taskId); if (reportFile == null) { response.setStatus(HttpStatus.NOT_FOUND.value()); return; } response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + reportFile.getName()); response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); StreamUtils.copy(new FileInputStream(reportFile), response.getOutputStream()); response.flushBuffer(); } // 异步服务实现 @Service public class ReportService { private final ConcurrentHashMap<String, ReportStatus> taskStatus = new ConcurrentHashMap<>(); private final String reportDir = "/tmp/reports/"; @Async public void generateReportAsync(String taskId) { taskStatus.put(taskId, ReportStatus.PROCESSING); try { List<Person> people = getPeople(); File reportFile = new File(reportDir + taskId + ".txt"); try (FileWriter writer = new FileWriter(reportFile)) { for (Person p : people) { writer.write(p.toString() + "\n"); } } taskStatus.put(taskId, ReportStatus.COMPLETED); } catch (Exception e) { taskStatus.put(taskId, ReportStatus.FAILED); } } public ReportStatus getTaskStatus(String taskId) { return taskStatus.getOrDefault(taskId, ReportStatus.NOT_FOUND); } public File getGeneratedReport(String taskId) { if (ReportStatus.COMPLETED.equals(getTaskStatus(taskId))) { return new File(reportDir + taskId + ".txt"); } return null; } public enum ReportStatus { PROCESSING, COMPLETED, FAILED, NOT_FOUND } }
客户端逻辑调整
- 调用
/generate-report获取任务ID; - 每隔2-3秒调用
/report-status/{taskId}查询状态; - 当状态为
COMPLETED时,调用/download-generated/{taskId}下载报表; - 若状态为
FAILED,提示用户生成失败。
3. 其他辅助优化
- 分块传输:Spring MVC默认支持HTTP分块传输(不设置
Content-Length时自动启用),确保响应不会因为一次性发送大文件被截断; - 数据过滤:允许用户通过参数过滤报表数据(比如按时间范围、地区),减少报表体积;
- 缓存报表:对重复请求的报表进行缓存(比如按请求参数生成缓存Key),避免重复生成。
内容的提问来源于stack exchange,提问作者AMagic
相关产品推荐
相关产品推荐

