从服务器下载1GB大文件时请求超时的解决方案咨询
你好!我来帮你解决这两个困扰你一周的问题——大文件下载时ServiceB的异步超时,以及直接从ServiceA下载时没有加载指示器的问题。咱们一步步来:
一、解决ServiceB的AsyncRequestTimeoutException(大文件下载超时)
这个异常的核心原因是Spring MVC默认的异步请求超时时间太短,没法覆盖大文件的传输时长。同时你的WebClient用法也有可以优化的地方,具体调整如下:
- 延长Spring MVC异步请求超时时间
默认情况下,Spring MVC的异步请求超时只有10秒左右,完全不够大文件传输。你可以通过两种方式修改:
- 配置文件方式(application.properties):
spring.mvc.async.request-timeout=3600000 # 设置为1小时,可根据你的最大文件大小调整
- Java配置类方式:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { // 设置默认超时时间为1小时 configurer.setDefaultTimeout(3600000); } }
- 给WebClient设置专属超时
除了Spring MVC的全局超时,还要给WebClient本身配置连接和读取超时,避免ServiceA端传输缓慢导致WebClient先触发超时:
// 创建自定义HttpClient,设置超时 HttpClient httpClient = HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // 连接超时5秒 .responseTimeout(Duration.ofHours(1)); // 读取超时1小时 // 用自定义HttpClient构建WebClient WebClient client = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .baseUrl(STORAGE_HOST) .build();
- 去掉block(),用反应式方式返回
你当前使用block()会阻塞线程,既浪费了反应式编程的优势,也容易引发线程池耗尽和超时问题。建议在ServiceB的控制器中直接返回反应式类型,让Spring MVC自动处理异步流:
@GetMapping("/public/download") public Mono<Void> downloadPublicFile(@RequestParam String requestId, HttpServletResponse response) { // 构建带超时配置的WebClient HttpClient httpClient = HttpClient.create().responseTimeout(Duration.ofHours(1)); WebClient client = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .baseUrl(STORAGE_HOST) .build(); // 获取ServiceA的文件流 Flux<DataBuffer> fileDataStream = client.get() .uri(uriBuilder -> uriBuilder.path("folders/zip/download") .queryParam("requestId", requestId) .build()) .accept(MediaType.APPLICATION_OCTET_STREAM) .retrieve() .bodyToFlux(DataBuffer.class); // 从ServiceA的响应头中获取文件信息,设置到客户端响应中 client.get() .uri(uriBuilder -> uriBuilder.path("folders/zip/download") .queryParam("requestId", requestId) .build()) .head() .subscribe(headers -> { response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); response.setContentLength(headers.getContentLength()); response.setHeader(HttpHeaders.ETAG, headers.getETag()); }); // 流式写入响应输出流,返回Mono<Void>让Spring MVC处理异步 return DataBufferUtils.write(fileDataStream, response.getOutputStream()) .map(DataBufferUtils::release) .then(); }
这样修改后,ServiceB会以完全异步的方式流式传输文件,避免阻塞线程,也能有效解决超时问题。
二、解决ServiceA下载无加载指示器的问题
直接从ServiceA下载时没有加载指示器,是因为InputStreamResource的处理方式无法让浏览器正确识别下载进度。建议改用StreamingResponseBody,它是Spring专门为流式输出设计的类型,能让浏览器根据你设置的Content-Length计算并显示下载进度:
修改ServiceA的返回代码:
File zipFile = new File(folderDownload.getZipPath()); try { // 创建StreamingResponseBody,逐块写入文件内容 StreamingResponseBody responseBody = outputStream -> { try (InputStream inputStream = new BufferedInputStream(new FileInputStream(zipFile))) { byte[] buffer = new byte[8192]; // 8KB缓冲区,可调整 int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); // 强制刷新输出流,确保内容及时发送到浏览器 outputStream.flush(); } } }; return ResponseEntity .status(HttpStatus.OK) .eTag(folderDownload.getDownloadId()) .contentLength(zipFile.length()) .lastModified(Instant.ofEpochMilli(zipFile.lastModified())) .body(responseBody); } catch (Exception e) { throw new DownloadException(zipFile.getName(), e.getMessage(), 400); }
StreamingResponseBody会逐块输出文件,结合你已经设置的Content-Length,浏览器就能准确计算下载进度,显示加载指示器了。
内容的提问来源于stack exchange,提问作者Kristjan
相关产品推荐
相关产品推荐

