Spring WebFlux中通过WebClient转发FilePart失败问题排查
问题重现
我在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

