Spring Boot微服务大文件下载失败,求可行优化实现方案
Spring Boot微服务大文件下载优化方案
问题描述
我在Spring Boot微服务应用中尝试下载大文件,当前实现无法处理300MB、400MB这类大文件,求更优实现方案。
当前代码实现
前端代码
<a href={path + curDoc.id} download></a>
网关层AJAX控制器代码
@GetMapping("/download-collector-file/{collectorFileId}") public ResponseEntity<Resource> downloadCollectorFile(@PathVariable String collectorFileId) { System.out.println("kjwenfjkewew beforr"); ResponseEntity<Resource> r= fileServiceAPI.downloadCollectorFile("token",collectorFileId, false); System.out.println("after api"); return r; }
Feign客户端代码
@GetMapping("/order-files/read/{id}") ResponseEntity<Resource> downloadCollectorFile(@RequestHeader("Authorization") String auth, @PathVariable String id, @RequestParam("isDownload") Boolean isDownload);
服务端后端API代码
@RequestMapping(value="/{id}" , method = RequestMethod.GET ) public ResponseEntity<InputStreamResource> downloadCollectorFile(@RequestHeader("Authorization") String auth, @PathVariable String id, @RequestParam(required = false, value = "isDownload") boolean isDownload) throws TgxValidationException, IOException { System.out.println("new file id------===>"+id); Optional<TgxFilesEntity> optionalTgxFilesEntity = tgxFilesRepository.findByIdAndDeletedFalse(id); if (optionalTgxFilesEntity.isPresent()) { TgxFilesEntity tgxFilesEntity = optionalTgxFilesEntity.get(); logger.info(">>> UPLOAD_FOLDER=" + UPLOAD_FOLDER_PATH); String absolutePath = UPLOAD_FOLDER_PATH + "/collector/" + tgxFilesEntity.getRelationalId() + "/"; final File parent = new File(absolutePath + tgxFilesEntity.getFilename()); InputStreamResource resource = new InputStreamResource(new FileInputStream(parent)); String mimeType = URLConnection.guessContentTypeFromName(tgxFilesEntity.getFilename()); logger.info("mimeType" + mimeType); HttpHeaders headers = new HttpHeaders(); if (!isDownload) { headers.add("Content-disposition", "inline; filename=" + tgxFilesEntity.getFilename()); if (mimeType == null) { int lastIndexOf = tgxFilesEntity.getFilename().lastIndexOf("."); if (tgxFilesEntity.getFilename().substring(lastIndexOf).contains(".json")) { mimeType = "application/json"; } else if (tgxFilesEntity.getFilename().substring(lastIndexOf).contains(".xlsx")) { mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; } else if (tgxFilesEntity.getFilename().substring(lastIndexOf).contains(".doc")) { mimeType = "application/msword"; } else if (tgxFilesEntity.getFilename().substring(lastIndexOf).contains(".zip")) { mimeType = "application/zip"; } else { mimeType = "text/plain"; } } } else { logger.info("hello else"); headers.add("Content-disposition", "attachment; filename=" + tgxFilesEntity.getFilename()); mimeType = "multipart/form-data"; } System.out.println("brefore return===>"+resource.getFilename()); return ResponseEntity.ok().headers(headers).contentType(MediaType.parseMediaType(mimeType)).body(resource); } else throw new FileNotFoundException(environment.getProperty("tgx.validation.file_not_found")); }
优化方案
核心问题分析
当前实现用InputStreamResource会把整个文件一次性加载到内存,大文件直接触发内存溢出(OOM),必须改成流式下载,逐块读取文件并输出到响应流,避免内存占用过高。
步骤1:修改服务端后端API(关键)
换成StreamingResponseBody实现流式输出,同时设置正确的文件大小和MIME类型:
@RequestMapping(value="/{id}", method = RequestMethod.GET) public ResponseEntity<StreamingResponseBody> downloadCollectorFile( @RequestHeader("Authorization") String auth, @PathVariable String id, @RequestParam(required = false, value = "isDownload") boolean isDownload) throws TgxValidationException, IOException { Optional<TgxFilesEntity> optionalTgxFilesEntity = tgxFilesRepository.findByIdAndDeletedFalse(id); if (!optionalTgxFilesEntity.isPresent()) { throw new FileNotFoundException(environment.getProperty("tgx.validation.file_not_found")); } TgxFilesEntity tgxFilesEntity = optionalTgxFilesEntity.get(); String absolutePath = UPLOAD_FOLDER_PATH + "/collector/" + tgxFilesEntity.getRelationalId() + "/"; File file = new File(absolutePath + tgxFilesEntity.getFilename()); // 确定MIME类型 String mimeType = URLConnection.guessContentTypeFromName(tgxFilesEntity.getFilename()); if (mimeType == null) { String suffix = tgxFilesEntity.getFilename().substring(tgxFilesEntity.getFilename().lastIndexOf(".")); switch (suffix.toLowerCase()) { case ".json": mimeType = "application/json"; break; case ".xlsx": mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; break; case ".doc": mimeType = "application/msword"; break; case ".zip": mimeType = "application/zip"; break; default: mimeType = "text/plain"; break; } } HttpHeaders headers = new HttpHeaders(); if (isDownload) { headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + tgxFilesEntity.getFilename() + "\""); } else { headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + tgxFilesEntity.getFilename() + "\""); } headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(file.length())); // 告诉浏览器文件大小 // 流式输出逻辑 StreamingResponseBody responseBody = outputStream -> { try (InputStream inputStream = new FileInputStream(file)) { byte[] buffer = new byte[4096]; // 4KB缓冲区,可根据服务器性能调整 int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); outputStream.flush(); // 实时输出,避免缓冲区积压 } } }; return ResponseEntity.ok() .headers(headers) .contentType(MediaType.parseMediaType(mimeType)) .body(responseBody); }
步骤2:修改Feign客户端
Feign默认不支持流式响应,改用Response接收并手动处理流:
@GetMapping("/order-files/read/{id}") Response downloadCollectorFile( @RequestHeader("Authorization") String auth, @PathVariable String id, @RequestParam("isDownload") Boolean isDownload);
步骤3:修改网关层控制器
直接转发流式响应,避免内存拷贝:
@GetMapping("/download-collector-file/{collectorFileId}") public void downloadCollectorFile( @PathVariable String collectorFileId, HttpServletResponse response) throws IOException { feign.Response feignResponse = fileServiceAPI.downloadCollectorFile("token", collectorFileId, false); // 拷贝响应头 feignResponse.headers().forEach((key, values) -> values.forEach(value -> response.addHeader(key, value))); // 流式拷贝响应体 try (InputStream inputStream = feignResponse.body().asInputStream(); OutputStream outputStream = response.getOutputStream()) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); outputStream.flush(); } } }
步骤4:前端优化(可选)
如果需要显示下载进度,用Fetch API替代<a>标签:
function downloadFile(fileId) { fetch(`/download-collector-file/${fileId}`, { method: 'GET' }).then(res => { const contentLength = parseInt(res.headers.get('Content-Length')); const reader = res.body.getReader(); let receivedLength = 0; const chunks = []; return reader.read().then(function processChunk({done, value}) { if (done) { // 合并chunk并下载 const blob = new Blob(chunks); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = res.headers.get('Content-Disposition').split('filename=')[1].replace(/"/g, ''); a.click(); URL.revokeObjectURL(url); return; } chunks.push(value); receivedLength += value.length; // 更新进度 const progress = (receivedLength / contentLength) * 100; console.log(`下载进度: ${progress.toFixed(2)}%`); return reader.read().then(processChunk); }); }); }
额外优化建议
- 支持断点续传:处理
Range请求头,实现分块下载,用户中断后可以继续下载。 - 调整服务器配置:
- Tomcat:在
application.properties设置server.tomcat.max-swallow-size=-1(禁用请求体大小限制)、server.tomcat.connection-timeout=600000(延长超时时间) - Jetty:调整
jetty.httpConfig.idleTimeout参数
- Tomcat:在
- 禁用响应缓存:添加
Cache-Control: no-cache, no-store响应头,避免大文件被缓存到内存。
内容的提问来源于stack exchange,提问作者Ultivic
相关产品推荐
相关产品推荐

