gridfsresource 默认不支持 http range 请求,导致视频无法拖拽播放;需手动解析range头、用gridfsbucket按偏移读取并设置正确响应头。

为什么直接用 GridFsResource 返回视频会卡顿或无法拖拽
因为 GridFsResource 默认不支持 HTTP Range 请求(即分段读取),浏览器发起视频播放时会先发 HEAD 请求探查 Content-Range 和 Accept-Ranges,而 Spring Boot 的默认 ResourceHttpRequestHandler 对 GridFsResource 不做范围解析,直接返回 200 + 全量流,导致无法边下边播、拖动失效、进度条灰色。
如何让 GridFsResource 支持 Range 请求
核心是重写资源处理逻辑,绕过默认的 ResourceHttpRequestHandler,手动解析 Range 头、计算偏移、用 GridFSBucket 的 downloadToStream 或 openDownloadStream 按需读取片段。
- 不要依赖
ResponseEntity<resource></resource>直接返回GridFsResource—— 它不会触发 Range 处理 - 用
GridFSBucket获取GridFSDownloadStream,配合HttpServletResponse手动写响应头和分块体 - 必须设置:
Content-Type(如video/mp4)、Accept-Ranges: bytes、Content-Range(对部分请求)、Content-Length(对全量)或Transfer-Encoding: chunked(对流式) - 注意 MongoDB 驱动版本:4.7+ 的
GridFSDownloadStream支持skip()和limit(),但更稳的方式是用seek()跳转到指定偏移再读
@GetMapping 中实现可拖拽的视频流响应
以下是一个生产可用的简化示例(假设文件 ID 已知,且已注入 GridFSBucket):
@GetMapping("/video/{id}")
public void streamVideo(@PathVariable ObjectId id, HttpServletRequest request, HttpServletResponse response) throws IOException {
GridFSFindIterable files = gridFSBucket.find(Filters.eq("_id", id));
GridFSFile file = files.first();
if (file == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
<pre class="brush:php;toolbar:false;">String contentType = Optional.ofNullable(file.getMetadata())
.map(m -> (String) m.get("contentType"))
.orElse("video/mp4");
long fileSize = file.getLength();
String range = request.getHeader("Range");
if (range != null && range.startsWith("bytes=")) {
// 解析 Range: bytes=1024-
String[] parts = range.substring(6).split("-");
long start = Long.parseLong(parts[0].trim());
long end = parts.length > 1 && !parts[1].trim().isEmpty()
? Long.parseLong(parts[1].trim()) : fileSize - 1;
if (end >= fileSize) end = fileSize - 1;
long length = end - start + 1;
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Content-Length", String.valueOf(length));
response.setContentType(contentType);
try (GridFSDownloadStream stream = gridFSBucket.openDownloadStream(id)) {
stream.skip(start);
StreamUtils.copy(new InputStream() {
@Override
public int read() throws IOException {
return stream.read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return stream.read(b, off, len);
}
}, response.getOutputStream());
}
} else {
// 全量响应
response.setHeader("Accept-Ranges", "bytes");
response.setContentLengthLong(fileSize);
response.setContentType(contentType);
gridFSBucket.downloadToStream(id, response.getOutputStream());
}}
容易被忽略的三个细节
很多实现跑通了但拖不动、卡在 00:00、或者移动端失败,问题往往出在这儿:
-
GridFSFile.getLength()必须准确 —— 如果文件是分块上传后未刷新元数据(比如用旧版驱动或手动 insert chunks),length可能为 0 或错乱,导致Content-Range计算崩溃 - 不要在
try-with-resources外提前 closeGridFSDownloadStream,MongoDB 驱动内部缓冲依赖流生命周期,提前关会导致后续 read 报IllegalStateException: Stream is closed - 某些安卓 WebView 或老版 iOS Safari 对
Content-Range格式敏感,务必确保空格和斜杠严格匹配:"bytes 0-1023/123456",不能多空格、不能少斜杠、不能用*/123456
真正麻烦的不是写流,是让每个字节都按浏览器预期的时间点抵达。Range 处理一旦错一位,整个拖动逻辑就退化成全量加载。











