Spring进阶 - 空指针安全、数据缓冲区和编解码器

书接上文

接下来到 Spring framework core 的剩余部分 —— 空指针安全、数据缓冲区和编解码器

空指针安全

尽管 Java 不允许您使用其类型系统来表示空指针安全性,但 Spring 框架现在在 org.springframework.lang 包中提供了以下注释,以使您声明 API 和字段的空性:

@Nullable:表示特定参数,返回值或字段可以为 null 的注释。

@NonNull:表示特定参数,返回值或字段不能为 null 的注释(@NonNullApi@NonNullFields 的参数/返回值和字段不需要)。

@NonNullApi:程序包级别的注释,它声明非 null 为参数和返回值的默认语义。

@NonNullFields:程序包级别的注释,它声明非 null 为字段的默认语义。

Spring 框架本身利用了这些注释,但是它们也可以在任何基于 Spring 的 Java 项目中使用,以声明 null 安全的 API 和可选的 null 安全的字段。尚不支持泛型类型参数,varargs 和数组元素的可空性,但应在即将发布的版本中使用它们,有关最新信息,请参见 SPR-15942。可空性声明预计将在 Spring Framework 版本之间进行微调,包括次要版本。在方法主体内部使用的类型的可空性超出了此功能的范围。

其他常见的库(如 Reactor 和 Spring Data)提供了使用类似可空性设置的空安全 API,从而为 Spring 应用程序开发人员提供了一致的总体体验。

用例

除了为 Spring Framework API 可空性提供显式声明外,IDE(例如 IDEA 或 Eclipse)还可以使用这些批注提供与空安全有关的有用警告,以避免在运行时出现 NullPointerException

由于 Kotlin 原生支持 null 安全,因此它们还用于在 Kotlin 项目中使 Spring API 为 null 安全。 Kotlin 支持文档中提供了更多详细信息。

JSR-305 元注释

Spring 注释使用 JSR 305 注释(静止但广泛使用的 JSR)进行元注释。 JSR-305 元注释使工具供应商(如 IDEA 或 Kotlin)以通用方式提供了空安全支持,而无需对 Spring 注释进行硬编码支持。

既不需要也不建议向项目类路径添加 JSR-305 依赖项以利用 Spring 空安全 API。只有诸如在其代码库中使用空安全注释的基于 Spring 的库之类的项目,才应添加 com.google.code.findbugs:jsr305:3.0.2(具有 compileOnly Gradle 配置或 Maven provided 的范围),以避免编译警告。

数据缓冲区和编解码器

Java NIO 提供了 ByteBuffer,但是许多库在上层构建了自己的字节缓冲区 API,特别是对于网络操作,其中重用缓冲区和/或使用直接缓冲区对性能有利。例如,Netty 具有 ByteBuf 层次结构,Undertow 使用 XNIO,Jetty 使用具有要释放的回调的池化字节缓冲区,等等。spring-core 模块提供了一组抽象,可以与各种字节缓冲区 API 配合使用,如下所示:

  • DataBufferFactory 抽象数据缓冲区的创建。
  • DataBuffer 表示一个字节缓冲区,可以将其合并。
  • DataBufferUtils 提供了用于数据缓冲区的实用程序方法。
  • 编解码器将流数据缓冲区流解码或编码为更高级别的对象。

DataBufferFactory

DataBufferFactory 用于通过以下两种方式之一创建数据缓冲区:

  1. 分配一个新的数据缓冲区,可以选择预先指定容量(如果已知),即使 DataBuffer 的实现可以按需增长和缩小,该容量也会更有效。
  2. 包装一个现有的 byte[]java.nio.ByteBuffer,它们用 DataBuffer 实现装饰给定的数据,并且不涉及分配。

请注意,WebFlux 应用程序不会直接创建 DataBufferFactory,而是通过客户端的 ServerHttpResponseClientHttpRequest 访问它。工厂的类型取决于基础客户端或服务器,例如 NettyDataBufferFactory 用于 Reactor Netty,DefaultDataBufferFactory 用于其他。

DataBuffer

DataBuffer 接口提供与 java.nio.ByteBuffer 类似的操作,但还带来了一些其他好处,其中一些是受 Netty ByteBuf 启发的。以下是部分好处:

  • 具有独立位置的读取和写入,即不需要调用 flip() 在读取和写入之间交替。
  • java.lang.StringBuilder 一样,容量可以按需扩展。
  • 通过 PooledDataBuffer 进行缓冲池和引用计数。
  • 将缓冲区作为 java.nio.ByteBufferInputStreamOutputStream 查看。
  • 确定给定字节的索引或最后一个索引。

PooledDataBuffer

如 Javadoc 中针对 ByteBuffer 所述,字节缓冲区可以是直接的也可以是非直接的。直接缓冲区可以驻留在 Java 堆之外,从而无需复制本机 I/O 操作。这使得直接缓冲区对于通过套接字接收和发送数据特别有用,但直接创建和释放它们的成本也更高,这导致了缓冲池的想法。

PooledDataBufferDataBuffer 的扩展,可帮助进行引用计数,这对于字节缓冲区池至关重要。它是如何工作的?分配 PooledDataBuffer 时,引用计数为 1。调用 keep() 会增加计数,而调用 release() 会减少计数。只要计数大于 0,就保证不会释放缓冲区。当计数减少到 0 时,可以释放池中的缓冲区,这实际上意味着将为缓冲区保留的内存返回到内存池。

请注意,在大多数情况下,与其直接在 PooledDataBuffer 上进行操作,不如在 DataBufferUtils 中使用方便的方法,该方法仅在为 PooledDataBuffer 的实例时才将释放或保留应用于 DataBuffer

8.4。 DataBufferUtils
DataBufferUtils 提供了许多实用程序方法来对数据缓冲区进行操作:

  • 将数据缓冲区流连接到单个缓冲区中,可能具有零个副本,例如通过复合缓冲区(如果基础字节缓冲区 API 支持的话)。
  • InputStream 或 NIO channel 转换为 Flux<DataBuffer>,反之亦然,将 Publisher<DataBuffer> 转换为 OutputStream 或 NIO channel
  • 如果缓冲区是 PooledDataBuffer 的实例,则释放或保留 DataBuffer 的方法。
  • 从字节流中跳过或获取,直到特定的字节数为止。

编解码器

org.springframework.core.codec 包提供以下策略接口:

  • Encoder,用于将 Publisher<T> 编码为数据缓冲区流。
  • Decoder,用于将 Publisher<DataBuffer> 解码为更高级别的对象流。

spring-core 模块提供 byte[]ByteBufferDataBufferResourceString 编码器和解码器实现。spring-web 模块添加了 Jackson JSON,Jackson Smile,JAXB2,Protocol Buffers 和其他编码器和解码器。请参阅 WebFlux 部分中的编解码器。

使用 DataBuffer

使用数据缓冲区时,必须特别小心以确保释放缓冲区,因为它们可能会被合并。我们将使用编解码器来说明其工作原理,但是这些概念会更普遍地应用。让我们看看编解码器在内部必须执行哪些操作来管理数据缓冲区。

在创建更高级别的对象之前,Decoder 是最后一个读取输入数据缓冲区的对象,因此,它必须按以下方式释放它们:

  1. 如果 Decoder 只是读取每个输入缓冲区并准备立即释放它,则可以通过 DataBufferUtils.release(dataBuffer)这样做。
  2. 如果 Decoder 使用的是 Flux 或 Mono 运算符(例如 flatMap,reduce 和其他在内部预取和缓存数据项的运算符),或者正在使用诸如 filter,skip 的运算符以及其他遗漏项的运算符,则 doOnDiscard(PooledDataBuffer.class,DataBufferUtils 必须将::: release)添加到组合链中,以确保在丢弃此类缓冲区之前将其释放,这也可能是错误或取消信号的结果。
  3. 如果解码器以任何其他方式保留一个或多个数据缓冲区,则它必须确保在完全读取时释放它们,或者在读取和释放缓存的数据缓冲区之前发生错误或取消信号的情况下。

请注意,DataBufferUtils#join 提供了一种安全有效的方法来将数据缓冲区流聚合到单个数据缓冲区中。同样,skipUntilByteCounttakeUntilByteCount 是供解码器使用的其他安全方法。

Encoder 分配其他人必须读取(和释放)的数据缓冲区。因此,Encoder 无事可做。但是,如果在使用数据填充缓冲区时发生序列化错误,则 Encoder 必须小心释放数据缓冲区。例如:

1
2
3
4
5
6
7
8
9
10
11
12
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 上运行时,有用于调试缓冲区泄漏的调试选项。