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

Spring Boot中WebClient发送multipart/form请求遇阻塞问题求助

问题描述

在Spring Boot中使用WebClient发送包含文件和字符串字段的multipart/form-data请求时,BlockHound检测到FileInputStream存在阻塞调用。尝试过builder.asyncPart()、Flux.then()、.subscribeOn(Schedulers.boundedElastic())等方法,也手动生成boundary添加到请求头,但均未解决问题。

相关代码

public Mono<ResponseEntity> anotherCall(){
    MultipartBodyBuilder builder = new MultipartBodyBuilder();
    builder.part("description", "description");
    builder.part("email", "email");
    builder.part("priority", 1);
    builder.part("status", 2);
    builder.part("subject", "subject");
    builder.part("company_id", 150000240766l);
  return Mono.defer(() -> {
             return this.webClient.getWebClient().post().uri(URI)
                     .headers(
                      httpHeaders -> {
                          httpHeaders.setBasicAuth("API_KEY", "X");
                      })
                     .bodyValue(builder.build())
                     .contentType(MediaType.MULTIPART_FORM_DATA)
                     .exchangeToMono(
                             response -> {
                                 final HttpStatus status = response.statusCode();
                                 return response
                                         .bodyToMono(Map.class)
                                         .defaultIfEmpty(Map.of())
                                         .map(
                                                 body -> {
                                                     if (status.is2xxSuccessful()) {
                                                         return ResponseEntity.ok().body(body);
                                                     } else {
                                                         return Error(body);
                                                     }
                                                 });
                             });

         });
}

BlockHound堆栈跟踪信息

reactor.blockhound.BlockingOperationError: Blocking call! java.io.FileInputStream#readBytes
        at java.base/java.io.FileInputStream.readBytes(FileInputStream.java)
        at java.base/java.io.FileInputStream.read(FileInputStream.java:276)
        at java.base/java.io.FilterInputStream.read(FilterInputStream.java:132)
        at java.base/sun.security.provider.NativePRNG$RandomIO.readFully(NativePRNG.java:425)
        at java.base/sun.security.provider.NativePRNG$RandomIO.ensureBufferValid(NativePRNG.java:528)
        at java.base/sun.security.provider.NativePRNG$RandomIO.implNextBytes(NativePRNG.java:547)
        at java.base/sun.security.provider.NativePRNG.engineNextBytes(NativePRNG.java:221)
        at java.base/java.security.SecureRandom.nextBytes(SecureRandom.java:758)
        at java.base/java.security.SecureRandom.next(SecureRandom.java:815)
        at java.base/java.util.Random.nextInt(Random.java:323)
        at org.springframework.util.MimeTypeUtils.generateMultipartBoundary(MimeTypeUtils.java:404)
        at org.springframework.http.codec.multipart.MultipartWriterSupport.generateMultipartBoundary(MultipartWriterSupport.java:105)
        at org.springframework.http.codec.multipart.MultipartHttpMessageWriter.writeMultipart(MultipartHttpMessageWriter.java:185)
        at org.springframework.http.codec.multipart.MultipartHttpMessageWriter.lambda$write$0(MultipartHttpMessageWriter.java:158)
        at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:152)
        at reactor.core.publisher.MonoFlatMap.subscribeOrReturn(MonoFlatMap.java:53)
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:57)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
        at reactor.netty.http.client.HttpClientConnect$HttpIOHandlerObserver.onStateChange(HttpClientConnect.java:441)
        at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:677)
        at reactor.netty.resources.DefaultPooledConnectionProvider$DisposableAcquire.run(DefaultPooledConnectionProvider.java:274)
        at io.netty.util.concurrent.AbstractEventExecutor.runTask$$$capture(AbstractEventExecutor.java:174)
        at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java)
        at io.netty.util.concurrent.AbstractEventExecutor.safeExecute$$$capture(AbstractEventExecutor.java:167)
        at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java)
        at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:470)
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:569)
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base/java.lang.Thread.run(Thread.java:833)
    
2023-01-17 09:02:07.040  WARN [,19587802144e28cfd5bee3752b442824,a602a51f0e98a86c]   [nio-8080-exec-6] c.i.p.e.api.GlobalExceptionHandler       : Blocking call! java.io.FileInputStream#readBytes; nested exception is reactor.blockhound.BlockingOperationError: Blocking call! java.io.FileInputStream#readBytes
    
org.springframework.web.reactive.function.client.WebClientRequestException: Blocking call! java.io.FileInputStream#readBytes; nested exception is reactor.blockhound.BlockingOperationError: Blocking call! java.io.FileInputStream#readBytes
        at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:141)
        Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    *__checkpoint ⇢ Request to POST URI[DefaultWebClient]
Original Stack Trace:
        at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:141)
        at reactor.core.publisher.MonoErrorSupplied.subscribe(MonoErrorSupplied.java:55)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4490)
        at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:103)
        at reactor.core.publisher.FluxPeek$PeekSubscriber.onError(FluxPeek.java:222)
        at reactor.core.publisher.FluxPeek$PeekSubscriber.onError(FluxPeek.java:222)
        at reactor.core.publisher.FluxPeek$PeekSubscriber.onError(FluxPeek.java:222)
        at reactor.core.publisher.MonoNext$NextSubscriber.onError(MonoNext.java:93)
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onError(ScopePassingSpanSubscriber.java:96)
        at reactor.core.publisher.MonoFlatMapMany$FlatMapManyMain.onError(MonoFlatMapMany.java:204)
        at reactor.core.publisher.SerializedSubscriber.onError(SerializedSubscriber.java:124)
        at reactor.core.publisher.FluxRetryWhen$RetryWhenMainSubscriber.whenError(FluxRetryWhen.java:225)
        at reactor.core.publisher.FluxRetryWhen$RetryWhenOtherSubscriber.onError(FluxRetryWhen.java:274)
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onError(ScopePassingSpanSubscriber.java:96)
        at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onError(FluxContextWrite.java:121)
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:415)
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onNext(FluxConcatMap.java:251)
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:89)
        at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107)
        at reactor.core.publisher.EmitterProcessor.drain(EmitterProcessor.java:537)
        at reactor.core.publisher.EmitterProcessor.tryEmitNext(EmitterProcessor.java:343)
        at reactor.core.publisher.SinkManySerialized.tryEmitNext(SinkManySerialized.java:100)
        at reactor.core.publisher.InternalManySink.emitNext(InternalManySink.java:27)
        at reactor.core.publisher.FluxRetryWhen$RetryWhenMainSubscriber.onError(FluxRetryWhen.java:190)
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onError(ScopePassingSpanSubscriber.java:96)
        at reactor.core.publisher.MonoCreate$DefaultMonoSink.error(MonoCreate.java:201)
        at reactor.netty.http.client.HttpClientConnect$HttpObserver.onUncaughtException(HttpClientConnect.java:399)
        at reactor.netty.ReactorNetty$CompositeConnectionObserver.onUncaughtException(ReactorNetty.java:670)
        at reactor.netty.resources.DefaultPooledConnectionProvider$DisposableAcquire.onUncaughtException(DefaultPooledConnectionProvider.java:205)
        at reactor.netty.resources.DefaultPooledConnectionProvider$PooledConnection.onUncaughtException(DefaultPooledConnectionProvider.java:454)
        at reactor.netty.http.client.HttpClientOperations.onOutboundError(HttpClientOperations.java:583)
        at reactor.netty.channel.ChannelOperations.onError(ChannelOperations.java:254)
        at reactor.core.publisher.Operators.error(Operators.java:198)
        at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:161)
        at reactor.core.publisher.MonoFlatMap.subscribeOrReturn(MonoFlatMap.java:53)
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:57)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
        at reactor.netty.http.client.HttpClientConnect$HttpIOHandlerObserver.onStateChange(HttpClientConnect.java:441)
        at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:677)
        at reactor.netty.resources.DefaultPooledConnectionProvider$DisposableAcquire.run(DefaultPooledConnectionProvider.java:274)
        at io.netty.util.concurrent.AbstractEventExecutor.runTask$$$capture(AbstractEventExecutor.java:174)
        at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java)
        at io.netty.util.concurrent.AbstractEventExecutor.safeExecute$$$capture(AbstractEventExecutor.java:167)
        at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java)
        at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:470)
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:569)
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: reactor.blockhound.BlockingOperationError: Blocking call! java.io.FileInputStream#readBytes
        at java.base/java.io.FileInputStream.readBytes(FileInputStream.java)
        at java.base/java.io.FileInputStream.read(FileInputStream.java:276)
        at java.base/java.io.FilterInputStream.read(FilterInputStream.java:132)
        at java.base/sun.security.provider.NativePRNG$RandomIO.readFully(NativePRNG.java:425)
        at java.base/sun.security.provider.NativePRNG$RandomIO.ensureBufferValid(NativePRNG.java:528)
        at java.base/sun.security.provider.NativePRNG$RandomIO.implNextBytes(NativePRNG.java:547)
        at java.base/sun.security.provider.NativePRNG.engineNextBytes(NativePRNG.java:221)
        at java.base/java.security.SecureRandom.nextBytes(SecureRandom.java:758)
        at java.base/java.security.SecureRandom.next(SecureRandom.java:815)
        at java.base/java.util.Random.nextInt(Random.java:323)
        at org.springframework.util.MimeTypeUtils.generateMultipartBoundary(MimeTypeUtils.java:404)
        at org.springframework.http.codec.multipart.MultipartWriterSupport.generateMultipartBoundary(MultipartWriterSupport.java:105)
        at org.springframework.http.codec.multipart.MultipartHttpMessageWriter.writeMultipart(MultipartHttpMessageWriter.java:185)
        at org.springframework.http.codec.multipart.MultipartHttpMessageWriter.lambda$write$0(MultipartHttpMessageWriter.java:158)
        at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:152)
        at reactor.core.publisher.MonoFlatMap.subscribeOrReturn(MonoFlatMap.java:53)
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:57)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
        at reactor.netty.http.client.HttpClientConnect$HttpIOHandlerObserver.onStateChange(HttpClientConnect.java:441)
        at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:677)
        at reactor.netty.resources.DefaultPooledConnectionProvider$DisposableAcquire.run(DefaultPooledConnectionProvider.java:274)
        at io.netty.util.concurrent.AbstractEventExecutor.runTask$$$capture(AbstractEventExecutor.java:174)
        at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java)
        at io.netty.util.concurrent.AbstractEventExecutor.safeExecute$$$capture(AbstractEventExecutor.java:167)
        at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java)
        at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:470)
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:569)
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base/java.lang.Thread.run(Thread.java:833)
解决方案

问题根源

从堆栈跟踪可以看出,阻塞并非来自文件上传逻辑,而是Spring生成multipart请求的boundary时,调用SecureRandom.nextInt()触发了底层NativePRNG的阻塞IO操作——它会读取系统的/dev/random设备,这个设备在熵不足时会阻塞线程,被BlockHound检测到。

解决方法

1. 切换非阻塞随机源

修改JVM启动参数,让SecureRandom使用非阻塞的/dev/urandom设备:

-Djava.security.egd=file:/dev/./urandom

该参数直接替换默认的随机源,避免生成boundary时的阻塞问题,是最简便的全局解决方案。

2. 预生成boundary并手动设置

在弹性线程池预生成boundary,再手动设置到请求头,跳过Spring自动生成逻辑:

public Mono<ResponseEntity> anotherCall(){
    MultipartBodyBuilder builder = new MultipartBodyBuilder();
    builder.part("description", "description");
    builder.part("email", "email");
    builder.part("priority", 1);
    builder.part("status", 2);
    builder.part("subject", "subject");
    builder.part("company_id", 150000240766l);

    // 在弹性线程池生成boundary,避免阻塞IO线程
    return Mono.fromCallable(MimeTypeUtils::generateMultipartBoundary)
            .subscribeOn(Schedulers.boundedElastic())
            .flatMap(boundary -> 
                this.webClient.getWebClient().post().uri(URI)
                        .headers(httpHeaders -> {
                            httpHeaders.setBasicAuth("API_KEY", "X");
                            // 手动指定带boundary的Content-Type
                            httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA.withBoundary(boundary));
                        })
                        .bodyValue(builder.build())
                        .exchangeToMono(response -> {
                            final HttpStatus status = response.statusCode();
                            return response
                                    .bodyToMono(Map.class)
                                    .defaultIfEmpty(Map.of())
                                    .map(body -> status.is2xxSuccessful() 
                                            ? ResponseEntity.ok().body(body) 
                                            : Error(body));
                        })
            );
}

3. 升级Spring版本

部分较新的Spring Boot版本(如2.7.x及以上)已优化multipart boundary生成逻辑,避免在非阻塞线程中触发

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 11:01:34