20220507 Core - 8. Data Buffers and Codecs
前言
Java NIO 提供了 ByteBuffer
,但是许多库在上面构建了自己的字节缓冲区 API,特别是对于重用缓冲区或使用直接缓冲区对性能有益的网络操作。例如,Netty 有 ByteBuf
,Undertow 使用 XNIO,Jetty 使用池化字节缓冲区,并带有要释放的回调,等等。
spring-core
模块提供了一组抽象来处理各种字节缓冲区 API,如下所示:
DataBufferFactory
抽象数据缓冲区的创建DataBuffer
表示一个字节缓冲区,它可以被池化DataBufferUtils
提供数据缓冲区的实用方法- 编解码器 Codecs 将流数据缓冲区流解码或编码为更高级别的对象
DataBufferFactory
DataBufferFactory
有两种方式创建数据缓冲区:
- 分配一个新的数据缓冲区,可以选择预先指定容量,即使
DataBuffer
的实现可以按需增长和缩小,这也会更高效 - 包装一个现有的
byte[]
或java.nio.ByteBuffer
,它用一个DataBuffer
实现装饰给定的数据并且不涉及分配内存
注意,WebFlux 应用程序不会直接创建 DataBufferFactory
,而是通过客户端的 ServerHttpResponse
或 ClientHttpRequest
访问它。工厂的类型取决于底层客户端或服务器,例如 NettyDataBufferFactory
对于 Reactor Netty ,DefaultDataBufferFactory
对于其他非 Netty 。
org.springframework.core.io.buffer.DataBufferFactory
DataBuffer
DataBuffer
接口提供了类似 java.nio.ByteBuffer
的操作,但也带来了一些额外的好处,其中一些受到 Netty ByteBuf
的启发:
- 以独立的位置读写,即不需要调用
flip()
在读取和写入之间交替 - 容量按需扩展,类似于
java.lang.StringBuilder
- 通过
PooledDataBuffer
来池化缓冲区和引用计数 - 不同的缓冲区视图,
java.nio.ByteBuffer
,InputStream
或OutputStream
- 确定给定字节的前数索引或后数索引
PooledDataBuffer
正如 ByteBuffer 的 javadoc 中所述 ,字节缓冲区可以是直接的或非直接的。直接缓冲区可能驻留在 Java 堆之外,这就不需要对本地 I/O 操作进行复制。这使得直接缓冲区对于通过套接字接收和发送数据特别有用,但它们的创建和释放成本也更高,这导致了缓冲池的想法。
PooledDataBuffer
是有助于引用计数的 DataBuffer
扩展,这对于字节缓冲池至关重要。它是如何工作的?当分配 PooledDataBuffer
时,引用计数为 1。调用 retain()
增加计数,而调用 release()
减少它。只要计数大于 0,就保证不会释放缓冲区。当计数减少到 0 时,可以释放缓冲池,这在实践中可能意味着为缓冲区保留的内存返回到内存池。
请注意,在大多数情况下,最好使用 DataBufferUtils
里方便的方法,而不是直接操作 PooledDataBuffer
,该方法仅在 DataBuffer
是 PooledDataBuffer
的实例时,才将 release
或 retain
应用于 DataBuffer
。
DataBufferUtils
DataBufferUtils
提供了许多实用方法来操作数据缓冲区:
- 如果底层字节缓冲区 API 支持,则将数据缓冲区流加入可能具有零拷贝的单个缓冲区,例如通过复合缓冲区
- 转型
InputStream
或 NIOChannel
为Flux
,反之亦可,转型Publisher<DataBuffer>
为OutputStream
或 NIOChannel
- 如果缓冲区是
PooledDataBuffer
的实例,则释放或保留DataBuffer
的方法 - 跳过或从字节流中取出,直到达到特定的字节数
编解码器 Codecs
org.springframework.core.codec
包提供了以下策略接口:
Encoder
:编码Publisher<T>
为数据缓冲区流Decoder
:解码Publisher<DataBuffer>
为更高级别的对象流
spring-core
模块提供 byte[]
、ByteBuffer
、DataBuffer
、Resource
和 String
编码器和解码器实现。spring-web
模块添加了 Jackson JSON、Jackson Smile、JAXB2、Protocol Buffers 等编码器和解码器。请参阅 WebFlux 部分中的编解码器。
使用 DataBuffer
使用数据缓冲区时,必须特别小心以确保缓冲区被释放,因为它们可能被池化。我们将使用编解码器来说明它是如何工作的,但这些概念应用得更广泛。让我们看看编解码器必须在内部做什么来管理数据缓冲区。
在创建更高级别的对象之前,Decoder
是最后一个读取输入数据缓冲区的,因此它必须按如下方式释放它们:
- 如果
Decoder
只是读取每个输入缓冲区并准备立即释放它,它可以通过DataBufferUtils.release(dataBuffer)
- 如果
Decoder
是使用Flux
或Mono
操作,如flatMap
,reduce
和其他该预取和高速缓存的数据项内部,或者是使用操作,如filter
,skip
即省略数据项,或其他,然后doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)
必须被添加到组合链以确保这些缓冲区在被丢弃之前被释放,这也可能导致错误或取消信号 - 如果
Decoder
以任何其他方式保留一个或多个数据缓冲区,则必须确保它们在完全读取时被释放,或者在读取和释放缓存数据缓冲区之前发生错误或取消信号的情况下释放它们
请注意,DataBufferUtils#join
提供了一种将数据缓冲区流聚合为单个数据缓冲区的安全有效的方法。同样 skipUntilByteCount
和 takeUntilByteCount
是解码器使用的额外安全方法。
Encoder
分配其他人必须读取(和释放)的数据缓冲区。所以 Encoder
没有什么可做的。但是,如果在用数据填充缓冲区时发生序列化错误,则 Encoder
必须小心释放数据缓冲区。例如:
DataBuffer buffer = factory.allocateBuffer();
boolean release = true;
try {
// serialize and populate buffer..
release = false;
}
finally {
if (release) {
DataBufferUtils.release(buffer);
}
}
return buffer;
Encoder
的使用者负责释放它接收到的数据缓冲区。在 WebFlux 应用程序中, Encoder
的输出用于写入 HTTP 服务器响应或客户端 HTTP 请求,在这种情况下,释放数据缓冲区是写入服务器响应或客户端请求的代码的责任
请注意,在 Netty 上运行时,有用于排除缓冲区泄漏的调试选项 。