问题
使用 SpringBoot + Webflux 框架开发的时候,用 WebClient 发起接受某次请求的时候发现了如下报错堆栈
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144
at org.springframework.core.io.buffer.LimitedDataBufferList.raiseLimitException(LimitedDataBufferList.java:99)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ Body from POST http://xxxxxxxxxx/ [DefaultClientResponse]
*__checkpoint ⇢ Handler cn.xx.xx.controller.xxxxxxx#xxxxxxx(ServerHttpRequest) [DispatcherHandler]
*__checkpoint ⇢ HTTP POST "/xxxx" [ExceptionHandlingWebHandler]
Original Stack Trace:
at org.springframework.core.io.buffer.LimitedDataBufferList.raiseLimitException(LimitedDataBufferList.java:99)
at org.springframework.core.io.buffer.LimitedDataBufferList.updateCount(LimitedDataBufferList.java:92)
at org.springframework.core.io.buffer.LimitedDataBufferList.add(LimitedDataBufferList.java:58)
at reactor.core.publisher.MonoCollect$CollectSubscriber.onNext(MonoCollect.java:103)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:200)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
at reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:379)
at reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:426)
at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:815)
at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:114)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318)
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
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:1583)
解析
webClient 发送的 请求体 或接受 响应体 过大(超过 256K) 的时候都会有这个问题。Spring WebFlux 限制 了编解码器(codec)中 Buffer 的内存大小,以避免应用出现内存问题。
Exceeded limit on max bytes to buffer : 262144
解决方案
1、添加 WebClienConfig
@Configuration
@Slf4j
public class WebClientConfig {
@Bean
public WebClient.Builder webClientBuilder() {
// 增加新配置,缓冲区大小为5MB
ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(5 * 1024 * 1024))
.build();
return WebClient.builder()
.exchangeStrategies(strategies)
.defaultHeader("User-Agent", "xxxx")
.filter((request, next) -> {
log.debug("Sending request to: {}", request.url());
return next.exchange(request);
});
}
}
或者
在 application.properties
OR application.yaml
中加配置
# fix Spring WebFlux DataBufferLimitException
spring.webflux.max-in-memory-size=5242880
spring.codec.max-in-memory-size=5242880
缺点
- 理论上没有解决问题,本质上是加资源的行为
2、压缩
考虑对 Req 或者 Resp 做压缩处理,不确定可行性,尝试 ing
拓展
一、核心概念:什么是编解码器(Codec)?
编解码器(Codec) 是 编码器(Encoder) 和 解码器(Decoder) 的合称。在 Spring WebFlux 的上下文中,它是一组用于在 HTTP 请求/响应的字节数据(Byte Buffer) 与 Java 对象 之间进行相互转换的组件。
- 解码(Decode): 将来自 HTTP 请求(如
POST
或PUT
的请求体)的原始字节流(DataBuffer
)反序列化 为一个 Java 对象。这个过程发生在你读取@RequestBody
或ServerRequest
的 body 时。 - 编码(Encode): 将你的 Java 对象(方法的返回值)序列化 为字节流,以便通过 HTTP 响应发送给客户端。这个过程发生在你返回一个对象或使用
ServerResponse
的 body 时。
简单来说,Codec 就是 Spring WebFlux 中处理序列化(Serialization)和反序列化(Deserialization)的机制。
二、为什么需要编解码器?(它的作用)
在传统的 Spring MVC(Servlet Stack)中,这个过程通常由 HttpMessageConverter 来处理。但在响应式、非阻塞的 WebFlux(Reactive Stack)中,底层是基于 Reactor 的 Publisher
(如 Flux
和 Mono
),数据是以数据流(Stream) 的形式异步处理的。
因此,传统的 HttpMessageConverter
接口(其核心方法是 read
和 write
,它们是阻塞式的)不再适用。WebFlux 需要一套全新的、非阻塞的、能够处理响应式流的机制来处理数据的编码和解码。这就是 Codec 诞生的原因。
它的核心作用包括:
- 处理响应式流(Reactive Streams): Codec 能够处理
Flux
和Mono
这样的发布者,实现流的逐块(chunk-by-chunk)编码和解码,而不是等待整个数据块加载完毕,这对于处理大文件或无限流至关重要。 - 非阻塞操作: 所有操作都是异步和非阻塞的,不会占用宝贵的线程资源,这与 WebFlux 的整体设计哲学一致。
- 支持多种数据格式: 开箱即用地支持主流数据格式的转换,如 JSON、XML、纯文本等。
- 提高性能: 通过高效的零拷贝(zero-copy)或内存映射等技术,在处理数据缓冲区(
DataBuffer
)时可以获得极高的性能。
三、核心组件:Encoder & Decoder
编解码器由两个独立的接口定义:
1. 解码器(org.springframework.core.codec.Decoder
)
- 职责: 将输入的
DataBuffer
流(Flux<DataBuffer>
)解码为对象的流(Flux<T>
)或单个对象(Mono<T>
)。 - 核心方法:
decode(Flux<DataBuffer> inputStream, ...)
- 常见实现:
Jackson2JsonDecoder
: 将 JSON 数据流解码为 POJO 对象。这是最常用的解码器。Jackson2SmileDecoder
: 处理 Smile 格式(二进制 JSON)。Jaxb2XmlDecoder
: 将 XML 数据流解码为通过 JAXB 注解的对象。StringDecoder
: 将字节流解码为字符串流。
2. 编码器(org.springframework.core.codec.Encoder
)
- 职责: 将对象的流(
Flux<T>
)或单个对象(Mono<T>
)编码为输出的DataBuffer
流(Flux<DataBuffer>
)。 - 核心方法:
encode(Flux<T> inputStream, ...)
- 常见实现:
Jackson2JsonEncoder
: 将 POJO 对象编码为 JSON 数据流。这是最常用的编码器。Jackson2SmileEncoder
: 将对象编码为 Smile 格式。Jaxb2XmlEncoder
: 将对象编码为 XML 数据流。CharSequenceEncoder
: 将字符串序列编码为字节流。
总结
特性 | 传统 Spring MVC (Servlet) | Spring WebFlux (Reactive) |
---|---|---|
转换机制 | HttpMessageConverter | Codec ** (Encoder + Decoder)** |
设计哲学 | 阻塞 I/O | 非阻塞、异步、响应式流 |
处理单位 | 完整的对象 | 数据流 (Flux<DataBuffer> ) |
核心接口 | HttpMessageConverter | Encoder<T> , Decoder<T> |
总而言之,Spring WebFlux 中的 Codec 是一套为响应式编程模型设计的、非阻塞的序列化和反序列化机制。它负责在 HTTP 协议的字节数据与你的 Java 对象之间进行高效、流式的转换,是构建响应式 REST API 的基石。在大多数情况下,Spring Boot 已经为你自动配置好了,你只需关注你的业务逻辑。