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

Spring WebFlux中通过WebClient转发FilePart失败问题排查

Spring WebFlux转发Multipart文件时接收端无法获取文件的问题

问题重现

我在WebFlux中接收Multipart请求的Flux<FilePart>,过滤后转发到另一个服务,但接收端无法拿到实际的"images"文件。通过日志确认文件已添加到MultipartBodyBuilder,但推测问题出在.asyncPart("images", part.content(), DataBuffer.class)的使用上。另外,将控制器参数改为List<FilePart>能解决问题,但担心这是WebFlux中的阻塞反模式。

控制器代码

@ResponseStatus(code = HttpStatus.CREATED)
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<String> create(
    @RequestPart(value = "images", required = false) final Flux<FilePart> images
) {
    return service.create(images);
}

服务代码

public Mono<String> create(final Flux<FilePart> images) {
    MultipartBodyBuilder builder = new MultipartBodyBuilder();

    return images
        .filter(part -> Objects.nonNull(part.headers().getContentType()))
        .filter(part ->
            part.headers().getContentType().includes(MediaType.IMAGE_JPEG)
                || part.headers().getContentType().includes(MediaType.IMAGE_PNG)
        )
        .doOnNext(part -> builder
                .asyncPart("images", part.content(), DataBuffer.class)
                .filename(part.filename())
        )
        .then(webClient.post()
            .uri(URI_RESOURCE)
            .contentType(MediaType.MULTIPART_FORM_DATA)
            .body(BodyInserters.fromMultipartData(builder.build()))
            .retrieve()
            .bodyToMono(String.class)
        );
}

问题原因

你当前代码的核心问题是:then()会等待前面的Flux<FilePart>流完全处理完成后才执行WebClient请求。而doOnNext只是将FilePart的content()(一个Flux<DataBuffer>)注册到MultipartBodyBuilder,但并没有订阅这个流。当WebClient最终订阅请求体时,原FilePart的数据流已经被取消或无法访问,导致接收端拿不到实际文件内容。

另外,控制器改用List<FilePart>确实能解决问题,但这会让WebFlux等待所有文件接收完成后再处理,本质是将整个请求的文件加载到内存(或临时文件)中,属于反模式——违背了WebFlux流式处理的初衷,在大文件/多文件场景下会引发内存压力或性能问题。

解决方案

方案1:流式转发(推荐,适合大量/大文件)

直接将过滤后的Flux<FilePart>转换为Flux<Part>,通过BodyInserters.fromPublisher流式构建Multipart请求体,确保文件内容在WebClient发送请求时才被订阅和传输:

public Mono<String> create(final Flux<FilePart> images) {
    // 将FilePart转换为Multipart Part
    Flux<Part> multipartParts = images
        .filter(part -> Objects.nonNull(part.headers().getContentType()))
        .filter(part ->
            part.headers().getContentType().includes(MediaType.IMAGE_JPEG)
                || part.headers().getContentType().includes(MediaType.IMAGE_PNG)
        )
        .map(filePart -> {
            HttpHeaders partHeaders = new HttpHeaders();
            partHeaders.setContentType(filePart.headers().getContentType());
            // 设置表单数据的Content-Disposition
            partHeaders.setContentDispositionFormData("images", filePart.filename());
            // 构建包含文件流的HttpEntity
            HttpEntity<Flux<DataBuffer>> partEntity = new HttpEntity<>(filePart.content(), partHeaders);
            // 创建Multipart Part
            return PartBuilder.create("images", partEntity.getBody())
                .headers(partEntity.getHeaders())
                .build();
        });

    return webClient.post()
        .uri(URI_RESOURCE)
        .contentType(MediaType.MULTIPART_FORM_DATA)
        // 直接用流式的Part构建请求体
        .body(BodyInserters.fromPublisher(multipartParts, Part.class))
        .retrieve()
        .bodyToMono(String.class);
}

方案2:收集后转发(适合少量小文件)

如果文件数量少且体积小,可以用collectList()将Flux<FilePart>收集为Mono<List<FilePart>>,再构建Multipart请求体。这种方式不会阻塞,但会将所有文件元信息暂存(文件内容仍为流式):

public Mono<String> create(final Flux<FilePart> images) {
    return images
        .filter(part -> Objects.nonNull(part.headers().getContentType()))
        .filter(part ->
            part.headers().getContentType().includes(MediaType.IMAGE_JPEG)
                || part.headers().getContentType().includes(MediaType.IMAGE_PNG)
        )
        // 收集所有符合条件的FilePart(仅收集元信息,文件内容仍为流式)
        .collectList()
        .flatMap(fileParts -> {
            MultipartBodyBuilder builder = new MultipartBodyBuilder();
            fileParts.forEach(part -> 
                builder.asyncPart("images", part.content(), DataBuffer.class)
                    .filename(part.filename())
                    .contentType(part.headers().getContentType())
            );
            return webClient.post()
                .uri(URI_RESOURCE)
                .contentType(MediaType.MULTIPART_FORM_DATA)
                .body(BodyInserters.fromMultipartData(builder.build()))
                .retrieve()
                .bodyToMono(String.class);
        });
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 05:20:22