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生成逻辑,避免在非阻塞线程中触发
相关产品推荐
相关产品推荐

