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

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

额外优化建议

  1. 支持断点续传:处理Range请求头,实现分块下载,用户中断后可以继续下载。
  2. 调整服务器配置:
    • Tomcat:在application.properties设置server.tomcat.max-swallow-size=-1(禁用请求体大小限制)、server.tomcat.connection-timeout=600000(延长超时时间)
    • Jetty:调整jetty.httpConfig.idleTimeout参数
  3. 禁用响应缓存:添加Cache-Control: no-cache, no-store响应头,避免大文件被缓存到内存。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 18:35:13