如何通过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
相关产品推荐
相关产品推荐

