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

如何通过Spring Boot REST API返回BLOB文件供前端下载?

Spring Boot BLOB文件下载对接Node栈前端实现方案

核心逻辑采用流式传输规避JVM内存溢出问题,通过标准HTTP二进制流协议传输,天然支持任意格式文件。

数据库层准备

你的文件表除了BLOB内容字段外,必须额外存储必要元信息字段,否则无法正确识别文件:

  • 主键ID:用于唯一定位文件
  • 原始文件名:带后缀,比如报销单.xls、说明.pdf
  • MIME类型:标记文件格式,比如application/pdf,如果历史数据没存这个字段,可以后续通过文件名后缀自动匹配
  • (可选)文件大小:方便前端展示下载进度

BLOB字段不要映射为byte[]类型,大文件会直接撑爆JVM堆内存,MyBatis/JPA都直接映射为java.sql.Blob类型即可,Mapper查询示例:

@Select("SELECT id, file_name, mime_type, file_blob FROM t_file WHERE id = #{fileId}")
@Results(@Result(column = "file_blob", property = "fileBlob", jdbcType = JdbcType.BLOB))
FileEntity getFileById(Long fileId);

Service层流处理逻辑

全程不把完整文件加载到内存,通过4KB缓冲区分段读写,代码如下:

public void streamFile(Long fileId, OutputStream out) throws SQLException, IOException {
    FileEntity file = fileMapper.getFileById(fileId);
    if (file == null) {
        throw new IllegalArgumentException("指定文件不存在");
    }
    try (InputStream in = file.getFileBlob().getBinaryStream();
         BufferedOutputStream bos = new BufferedOutputStream(out)) {
        byte[] buffer = new byte[4096];
        int readLen;
        while ((readLen = in.read(buffer)) != -1) {
            bos.write(buffer, 0, readLen);
        }
        bos.flush();
    }
}

如果没存MIME类型,直接用JDK自带工具匹配即可,匹配失败统一用通用二进制流类型:

String mime = URLConnection.guessContentTypeFromName(file.getFileName());
if (mime == null) {
    mime = "application/octet-stream";
}

Controller层接口实现

不要把文件内容封装进JSON返回,直接写入HTTP响应体,响应头必须配置正确避免乱码和格式识别错误:

@GetMapping("/file/download/{fileId}")
public void download(@PathVariable Long fileId, HttpServletResponse response) throws IOException, SQLException {
    FileEntity file = fileMapper.getFileById(fileId);
    if (file == null) {
        response.sendError(404, "文件不存在");
        return;
    }
    // 处理中文文件名乱码,遵循RFC 5987标准,全端兼容
    String encodedName = URLEncoder.encode(file.getFileName(), StandardCharsets.UTF_8).replace("+", "%20");
    // 设置响应头
    response.setContentType(file.getMimeType());
    response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedName);
    response.setHeader("Content-Length", String.valueOf(file.getFileBlob().length()));
    response.setHeader("Cache-Control", "no-store");
    // 输出流
    fileService.streamFile(fileId, response.getOutputStream());
}

注意:下载接口要排除全局响应包装、全局序列化拦截,否则会把二进制流转成JSON字符串,导致文件损坏。

Node侧接收处理

不管是Node中间层转发还是Node服务直接调用,都不要把响应当成字符串解析,直接用流方式接收,axios调用示例:

const axios = require('axios');
const fs = require('fs');

async function pullFile(fileId, localSavePath) {
  const res = await axios.get(`http://springboot-service/file/download/${fileId}`, {
    responseType: 'stream'
  });
  // 从响应头解析文件名
  const disposition = res.headers['content-disposition'];
  const fileName = decodeURIComponent(disposition.match(/filename\*=UTF-8''(.+)/)[1]);
  // 写本地或者转发给前端
  const writeStream = fs.createWriteStream(`${localSavePath}/${fileName}`);
  res.data.pipe(writeStream);
  return new Promise((resolve, reject) => {
    writeStream.on('finish', () => resolve(fileName));
    writeStream.on('error', reject);
  });
}

如果是Node层转发给浏览器端,直接把接收到的流pipe到Node服务的响应对象即可,Spring Boot返回的响应头原样透传,不需要额外转码。

常见坑点

  • 单文件超过100MB时,建议给nginx反向代理加proxy_buffering off;配置,避免nginx缓存大文件拖慢传输速度。
  • 不要在接口上加@ResponseBody注解或者返回byte[]/String类型,会被Spring消息转换器转码损坏文件。
  • 如果测试时发现pdf、xls等文件打开提示损坏,优先检查是不是有全局过滤器、响应切面修改了响应体内容。
  • 文件名编码必须用filename*=UTF-8''的标准写法,老的filename=字段在不同浏览器、不同Node版本下会出现中文乱码。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 03:18:28