Spring Webflux org.springframework.core.io.buffer.DataBufferLimitException 问题深度解析

问题

使用 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 请求(如 POSTPUT 的请求体)的原始字节流(DataBuffer反序列化 为一个 Java 对象。这个过程发生在你读取 @RequestBodyServerRequest 的 body 时。
  • 编码(Encode): 将你的 Java 对象(方法的返回值)序列化 为字节流,以便通过 HTTP 响应发送给客户端。这个过程发生在你返回一个对象或使用 ServerResponse 的 body 时。

简单来说,Codec 就是 Spring WebFlux 中处理序列化(Serialization)和反序列化(Deserialization)的机制


二、为什么需要编解码器?(它的作用)

在传统的 Spring MVC(Servlet Stack)中,这个过程通常由 HttpMessageConverter 来处理。但在响应式、非阻塞的 WebFlux(Reactive Stack)中,底层是基于 Reactor 的 Publisher(如 FluxMono),数据是以数据流(Stream) 的形式异步处理的。

因此,传统的 HttpMessageConverter 接口(其核心方法是 readwrite,它们是阻塞式的)不再适用。WebFlux 需要一套全新的、非阻塞的、能够处理响应式流的机制来处理数据的编码和解码。这就是 Codec 诞生的原因。

它的核心作用包括:

  1. 处理响应式流(Reactive Streams): Codec 能够处理 FluxMono 这样的发布者,实现流的逐块(chunk-by-chunk)编码和解码,而不是等待整个数据块加载完毕,这对于处理大文件或无限流至关重要。
  2. 非阻塞操作: 所有操作都是异步和非阻塞的,不会占用宝贵的线程资源,这与 WebFlux 的整体设计哲学一致。
  3. 支持多种数据格式: 开箱即用地支持主流数据格式的转换,如 JSON、XML、纯文本等。
  4. 提高性能: 通过高效的零拷贝(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)
转换机制HttpMessageConverterCodec** (Encoder + Decoder)**
设计哲学阻塞 I/O非阻塞、异步、响应式流
处理单位完整的对象数据流 (Flux<DataBuffer>)
核心接口HttpMessageConverterEncoder<T>, Decoder<T>

总而言之,Spring WebFlux 中的 Codec 是一套为响应式编程模型设计的、非阻塞的序列化和反序列化机制。它负责在 HTTP 协议的字节数据与你的 Java 对象之间进行高效、流式的转换,是构建响应式 REST API 的基石。在大多数情况下,Spring Boot 已经为你自动配置好了,你只需关注你的业务逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

.Shu.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值