如何用Spring WebFlux代理URL视频资源实现流式传输?能否改用Servlet?
实现WebFlux代理远程视频流式传输及Servlet替代方案
WebFlux 实现方案
你当前基于Spring WebFlux开发,推荐用WebClient实现代理流式传输,同时必须支持HTTP Range请求(视频播放必备的断点续传功能),具体实现步骤如下:
1. 配置WebClient实例
先创建WebClient用于发起远程资源请求:
@Configuration public class WebClientConfig { @Bean public WebClient webClient() { return WebClient.builder() // 可根据需求添加连接超时、读取超时等配置 .build(); } }
2. 编写视频代理接口
在控制器中实现代理逻辑,转发Range请求头到远程服务器,并将响应流式返回给前端:
@RestController @RequestMapping("/video") @Slf4j public class VideoProxyController { private final WebClient webClient; public VideoProxyController(WebClient webClient) { this.webClient = webClient; } @GetMapping(value = "/{videoId}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public Mono<ResponseEntity<Flux<DataBuffer>>> proxyVideo(@PathVariable String videoId, @RequestHeader(value = "Range", required = false) String rangeHeader) { // 根据videoId映射合法远程URL,务必做校验,防止恶意请求 String remoteVideoUrl = getValidRemoteUrl(videoId); if (remoteVideoUrl == null) { return Mono.just(ResponseEntity.notFound().build()); } // 构建远程请求,转发Range头 WebClient.RequestHeadersSpec<?> requestSpec = webClient.get() .uri(remoteVideoUrl); if (rangeHeader != null) { requestSpec.header("Range", rangeHeader); } return requestSpec.exchangeToMono(response -> { // 复制远程响应的关键头信息 HttpHeaders responseHeaders = new HttpHeaders(); response.headers().asHttpHeaders().forEach((key, value) -> { // 只转发视频播放必要的响应头 if (key.equalsIgnoreCase("Content-Type") || key.equalsIgnoreCase("Content-Length") || key.equalsIgnoreCase("Accept-Ranges") || key.equalsIgnoreCase("Content-Range")) { responseHeaders.put(key, value); } }); return Mono.just(ResponseEntity.status(response.statusCode()) .headers(responseHeaders) .body(response.bodyToFlux(DataBuffer.class))); }); } // 示例:根据videoId获取合法远程URL,实际可从数据库/配置中心读取 private String getValidRemoteUrl(String videoId) { // 这里需严格校验videoId,禁止直接拼接用户输入生成任意URL return "http://xxxx.com/video/" + videoId + ".mp4"; } }
核心注意点
- Range请求处理:视频播放时前端会分段请求资源,代理必须转发
Range头到远程服务器,同时回传Content-Range等响应头,保证断点续传功能正常。 - 安全校验:
getValidRemoteUrl方法要严格限制videoId对应的远程URL范围,避免被用来发起恶意远程请求。
Servlet(Spring MVC)实现方案
如果切换到Servlet栈(Spring MVC),可以用StreamingResponseBody实现流式输出,同样支持Range请求:
1. 配置RestTemplate实例
@Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
2. 编写代理接口
@RestController @RequestMapping("/video") @Slf4j public class VideoProxyMvcController { private final RestTemplate restTemplate; public VideoProxyMvcController(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @GetMapping(value = "/{videoId}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public ResponseEntity<StreamingResponseBody> proxyVideo(@PathVariable String videoId, @RequestHeader(value = "Range", required = false) String rangeHeader, HttpServletResponse response) { String remoteVideoUrl = getValidRemoteUrl(videoId); if (remoteVideoUrl == null) { return ResponseEntity.notFound().build(); } // 构建远程请求头 HttpHeaders requestHeaders = new HttpHeaders(); if (rangeHeader != null) { requestHeaders.add("Range", rangeHeader); } HttpEntity<Void> requestEntity = new HttpEntity<>(requestHeaders); // 发起远程请求获取响应流 ResponseEntity<InputStreamResource> remoteResponse = restTemplate.exchange( remoteVideoUrl, HttpMethod.GET, requestEntity, InputStreamResource.class); // 复制远程响应头到前端 remoteResponse.getHeaders().forEach((key, value) -> { if (key.equalsIgnoreCase("Content-Type") || key.equalsIgnoreCase("Content-Length") || key.equalsIgnoreCase("Accept-Ranges") || key.equalsIgnoreCase("Content-Range")) { response.setHeader(key, value.get(0)); } }); // 流式输出视频内容 StreamingResponseBody streamResponseBody = outputStream -> { try (InputStream inputStream = remoteResponse.getBody().getInputStream()) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); outputStream.flush(); } } catch (IOException e) { log.error("视频流传输异常", e); } }; return ResponseEntity.status(remoteResponse.getStatusCode()) .body(streamResponseBody); } private String getValidRemoteUrl(String videoId) { return "http://xxxx.com/video/" + videoId + ".mp4"; } }
核心注意点
StreamingResponseBody可以避免将整个视频加载到内存,适合大文件流式传输场景。- 同样需要处理
Range请求头,保证视频播放的断点续传功能正常。
内容的提问来源于stack exchange,提问作者Aster Casc
相关产品推荐
相关产品推荐

