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

如何用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 11:45:35