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

REST流式传输大文件遇DataBufferLimitException问题排查

问题根源

你的代码没实现真正的流式传输,核心问题出在findById方法里:

  • webClient.toEntity(InputStreamResource.class).block()会让WebClient把后端返回的整个文件内容全部加载到内存中,再封装成InputStreamResource返回。这就导致大文件直接触发DataBufferLimitException——因为Spring WebClient默认的响应体缓冲上限就是256KB。
  • 后面的StreamingOutput只是把内存里的数据流出去,本质还是先下载完整文件再转发,没有达到流式传输的目的。
修复方案

要实现真正的流式转发,需要让WebClient以数据流(Flux<DataBuffer>)的形式获取后端响应,再直接把数据流传递给客户端,全程不把完整文件加载到内存。

修改后的完整代码

@Component
@Api
@PreAuthorize("hasAuthority('USER')")
@RequiredArgsConstructor
public class AttachmentApiImpl implements AttachmentApi {
    private static final Logger LOGGER = LoggerFactory.getLogger(AttachmentApiImpl.class);
    private final WebClient webClient;

    @Override
    public Response getAttachment(String id) {
        // 同时获取响应头和数据流,不提前加载整个文件
        Pair<HttpHeaders, Flux<DataBuffer>> responseData = fetchAttachmentStream(id).block();
        if (responseData == null) {
            throw new WebApplicationException("Attachment not found", Response.Status.NOT_FOUND);
        }
        HttpHeaders backendHeaders = responseData.getFirst();
        Flux<DataBuffer> dataBufferFlux = responseData.getSecond();

        // 实现流式输出,逐块写入客户端响应
        StreamingOutput streamingOutput = output -> {
            DataBufferUtils.write(dataBufferFlux, output)
                    // 错误时释放未处理的DataBuffer,避免内存泄漏
                    .doOnError(error -> {
                        DataBufferUtils.release(dataBufferFlux);
                        LOGGER.error("Failed to stream attachment", error);
                        throw new WebApplicationException("Error streaming file", error);
                    })
                    // 阻塞直到整个流传输完成
                    .block();
        };

        return Response.ok(streamingOutput)
                .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.ETAG + ", " + HttpHeaders.CONTENT_DISPOSITION)
                .header(HttpHeaders.ETAG, backendHeaders.getETag())
                .header(HttpHeaders.CONTENT_DISPOSITION, backendHeaders.getContentDisposition().toString())
                .header(HttpHeaders.CONTENT_TYPE, backendHeaders.getContentType().toString())
                .build();
    }

    private Mono<Pair<HttpHeaders, Flux<DataBuffer>>> fetchAttachmentStream(String id) {
        return webClient.get()
                .uri("/attachments/{id}", id)
                .exchangeToMono(response -> {
                    // 获取后端响应头
                    HttpHeaders headers = response.headers().asHttpHeaders();
                    // 以数据流形式获取响应体,不缓冲到内存
                    Flux<DataBuffer> bodyStream = response.bodyToFlux(DataBuffer.class);
                    return Mono.just(Pair.of(headers, bodyStream));
                });
    }
}

关键改动说明

  1. 改用exchangeToMono获取响应头和数据流
    • 避免toEntity或retrieve().bodyToMono()这类会把完整响应加载到内存的方法,直接通过bodyToFlux(DataBuffer.class)获取流式响应体。
  2. 用DataBufferUtils处理数据流
    • DataBufferUtils.write可以把Flux<DataBuffer>直接写入输出流,实现逐块传输,全程不加载完整文件到内存。
  3. 添加DataBuffer释放逻辑
    • 在错误时调用DataBufferUtils.release,避免未消费的DataBuffer占用内存,导致泄漏。
额外优化建议
  • 不要在WebClient调用中使用block():如果你的JAX-RS框架支持异步响应,可以改为返回CompletionStage<Response>,用Mono的异步操作替代阻塞调用,提升并发性能。
  • 调整WebClient的超时设置:大文件传输耗时较长,可能需要增大WebClient的连接超时和读取超时,避免中途断开。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 18:23:14