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

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
    }
}

客户端逻辑调整

  1. 调用/generate-report获取任务ID;
  2. 每隔2-3秒调用/report-status/{taskId}查询状态;
  3. 当状态为COMPLETED时,调用/download-generated/{taskId}下载报表;
  4. 若状态为FAILED,提示用户生成失败。

3. 其他辅助优化

  • 分块传输:Spring MVC默认支持HTTP分块传输(不设置Content-Length时自动启用),确保响应不会因为一次性发送大文件被截断;
  • 数据过滤:允许用户通过参数过滤报表数据(比如按时间范围、地区),减少报表体积;
  • 缓存报表:对重复请求的报表进行缓存(比如按请求参数生成缓存Key),避免重复生成。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 21:54:55