响应式核心

spring-web 模块包含对响应式 Web 应用程序的以下基础支持:

  • 对于服务器请求处理,有两种级别的支持。

    • HttpHandler:使用非阻塞 I/O 和 Reactive Streams 背压处理 HTTP 请求的基本契约,以及对 Reactor Netty、Tomcat、Jetty 和任何 Servlet 容器的适配器。

    • WebHandler API:略高一级的通用 Web API,用于请求处理,在其之上构建了具体的编程模型,例如注解控制器和函数式端点。

  • 对于客户端,有一个基本的 ClientHttpConnector 契约,用于使用非阻塞 I/O 和 Reactive Streams 背压执行 HTTP 请求,以及对 Reactor Netty、响应式 Jetty HttpClientApache HttpComponents 的适配器。 应用程序中使用的更高级别的 WebClient 基于此基本契约。

  • 对于客户端和服务器,用于 HTTP 请求和响应内容序列化和反序列化的 编解码器

HttpHandler

HttpHandler 是一个简单的契约,只有一个方法用于处理请求和响应。它刻意保持最小化,其主要也是唯一目的是作为不同 HTTP 服务器 API 的最小抽象。

下表描述了支持的服务器 API:

服务器名称 使用的服务器 API Reactive Streams 支持

Netty

Netty API

Reactor Netty

Tomcat

Servlet 非阻塞 I/O;Tomcat API 用于读写 ByteBuffers 而非 byte[]

spring-web:Servlet 非阻塞 I/O 到 Reactive Streams 桥接

Jetty

Servlet 非阻塞 I/O;Jetty API 用于写入 ByteBuffers 而非 byte[]

spring-web:Servlet 非阻塞 I/O 到 Reactive Streams 桥接

Servlet 容器

Servlet 非阻塞 I/O

spring-web:Servlet 非阻塞 I/O 到 Reactive Streams 桥接

下表描述了服务器依赖项(另请参阅 支持的版本):

服务器名称 组 ID Artifact 名称

Reactor Netty

io.projectreactor.netty

reactor-netty

Tomcat

org.apache.tomcat.embed

tomcat-embed-core

Jetty

org.eclipse.jetty

jetty-server, jetty-servlet

下面的代码片段展示了如何将 HttpHandler 适配器与每个服务器 API 一起使用。

Reactor Netty

  • Java

  • Kotlin

HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bindNow();
val handler: HttpHandler = ...
val adapter = ReactorHttpHandlerAdapter(handler)
HttpServer.create().host(host).port(port).handle(adapter).bindNow()

Tomcat

  • Java

  • Kotlin

HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);

Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();
val handler: HttpHandler = ...
val servlet = TomcatHttpHandlerAdapter(handler)

val server = Tomcat()
val base = File(System.getProperty("java.io.tmpdir"))
val rootContext = server.addContext("", base.absolutePath)
Tomcat.addServlet(rootContext, "main", servlet)
rootContext.addServletMappingDecoded("/", "main")
server.host = host
server.setPort(port)
server.start()

Jetty

  • Java

  • Kotlin

HttpHandler handler = ...
JettyCoreHttpHandlerAdapter adapter = new JettyCoreHttpHandlerAdapter(handler);

Server server = new Server();
server.setHandler(adapter);

ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);

server.start();
val handler: HttpHandler = ...
val adapter = JettyCoreHttpHandlerAdapter(handler)

val server = Server()
server.setHandler(adapter)

val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)

server.start()

在 Spring Framework 6.2 中,JettyHttpHandlerAdapter 已弃用,取而代之的是 JettyCoreHttpHandlerAdapter,它直接与 Jetty 12 API 集成,无需 Servlet 层。

要作为 WAR 部署到 Servlet 容器,请使用 AbstractReactiveWebInitializer, 通过 ServletHttpHandlerAdapterHttpHandler 适配为 Servlet

WebHandler API

org.springframework.web.server 包基于 HttpHandler 契约 构建,提供了一个通用的 Web API,用于通过多个 WebExceptionHandler、多个 WebFilter 和单个 WebHandler 组件链处理请求。该链可以通过 WebHttpHandlerBuilder 组装,只需指向一个 Spring ApplicationContext,其中组件是 自动检测的,和/或通过构建器注册组件。

虽然 HttpHandler 的目标是抽象不同 HTTP 服务器的使用,但 WebHandler API 旨在提供 Web 应用程序中常用的更广泛的功能集,例如:

  • 带属性的用户会话。

  • 请求属性。

  • 已解析的请求 LocalePrincipal

  • 访问已解析和缓存的表单数据。

  • 多部分数据的抽象。

  • 以及更多…​

特殊 bean 类型

下表列出了 WebHttpHandlerBuilder 可以在 Spring ApplicationContext 中自动检测到,或可以直接向其注册的组件:

Bean 名称 Bean 类型 数量 描述

<any>

WebExceptionHandler

0..N

提供对 WebFilter 实例链和目标 WebHandler 抛出的异常的处理。有关更多详细信息,请参阅 异常

<any>

WebFilter

0..N

在过滤器链的其余部分和 目标 WebHandler 之前和之后应用拦截式逻辑。有关更多详细信息,请参阅 过滤器

webHandler

WebHandler

1

请求的处理程序。

webSessionManager

WebSessionManager

0..1

通过 ServerWebExchange 上的方法公开的 WebSession 实例的管理器。 默认情况下为 DefaultWebSessionManager

serverCodecConfigurer

ServerCodecConfigurer

0..1

用于访问 HttpMessageReader 实例,以便解析表单数据和多部分数据,然后 通过 ServerWebExchange 上的方法公开。默认情况下为 ServerCodecConfigurer.create()

localeContextResolver

LocaleContextResolver

0..1

通过 ServerWebExchange 上的方法公开的 LocaleContext 的解析器。 默认情况下为 AcceptHeaderLocaleContextResolver

forwardedHeaderTransformer

ForwardedHeaderTransformer

0..1

用于处理转发类型标头,通过提取并删除它们或仅删除它们。 默认情况下不使用。

表单数据

ServerWebExchange 公开以下方法用于访问表单数据:

  • Java

  • Kotlin

Mono<MultiValueMap<String, String>> getFormData();
suspend fun getFormData(): MultiValueMap<String, String>

DefaultServerWebExchange 使用配置的 HttpMessageReader 将表单数据 (application/x-www-form-urlencoded) 解析为 MultiValueMap。默认情况下, FormHttpMessageReader 被配置为由 ServerCodecConfigurer bean 使用 (参见 Web Handler API)。

多部分数据

ServerWebExchange 公开以下方法用于访问多部分数据:

  • Java

  • Kotlin

Mono<MultiValueMap<String, Part>> getMultipartData();
suspend fun getMultipartData(): MultiValueMap<String, Part>

DefaultServerWebExchange 使用配置的 HttpMessageReader<MultiValueMap<String, Part>>multipart/form-datamultipart/mixedmultipart/related 内容解析为 MultiValueMap。 默认情况下,这是 DefaultPartHttpMessageReader,它没有任何第三方依赖。 或者,可以使用 SynchronossPartHttpMessageReader,它基于 Synchronoss NIO Multipart 库。 两者都通过 ServerCodecConfigurer bean 配置 (参见 Web Handler API)。

要以流式方式解析多部分数据,您可以使用 PartEventHttpMessageReader 返回的 Flux<PartEvent>,而不是使用 @RequestPart,因为 @RequestPart 意味着通过名称对各个部分进行 Map 访问,因此需要完整解析多部分数据。 相比之下,您可以使用 @RequestBody 将内容解码为 Flux<PartEvent>,而无需收集到 MultiValueMap

转发标头

Unresolved directive in reactive-spring.adoc - include::partial$web/forwarded-headers.adoc[]

ForwardedHeaderTransformer

ForwardedHeaderTransformer 是一个组件,它根据转发的标头修改请求的主机、端口和方案,然后删除这些标头。如果您将其声明为名为 forwardedHeaderTransformer 的 bean,它将被 检测并使用。

安全注意事项

转发标头存在安全考虑,因为应用程序无法知道标头是由代理按预期添加的,还是由恶意客户端添加的。这就是为什么应配置信任边界处的代理以删除来自外部的不可信转发流量。您还可以将 ForwardedHeaderTransformer 配置为 removeOnly=true,在这种情况下,它会删除但不使用标头。

过滤器

WebHandler API 中,您可以使用 WebFilter 在过滤器处理链的其余部分和目标 WebHandler 之前和之后应用拦截式逻辑。当使用 WebFlux 配置 时,注册 WebFilter 就像将其声明为 Spring bean 并(可选)通过在 bean 声明上使用 @Order 或实现 Ordered 来表达优先级一样简单。

CORS

Spring WebFlux 通过控制器上的注解提供对 CORS 配置的细粒度支持。但是,当您将其与 Spring Security 一起使用时,我们建议依赖内置的 CorsFilter,它必须在 Spring Security 过滤器链之前排序。

有关更多详细信息,请参阅 CORS 部分和 CORS WebFilter

URL 处理程序

您可能希望您的控制器端点匹配 URL 路径中带或不带斜杠的路由。 例如,"GET /home" 和 "GET /home/" 都应该由用 @GetMapping("/home") 注解的控制器方法处理。

将尾部斜杠变体添加到所有映射声明并不是处理此用例的最佳方法。 UrlHandlerFilter web 过滤器就是为此目的而设计的。它可以配置为:

  • 当接收到带尾部斜杠的 URL 时,用 HTTP 重定向状态响应,将浏览器发送到不带尾部斜杠的 URL 变体。

  • 修改请求,使其表现得像没有尾部斜杠的请求一样,并继续处理请求。

以下是如何为博客应用程序实例化和配置 UrlHandlerFilter

异常

WebHandler API 中,您可以使用 WebExceptionHandler 来处理 WebFilter 实例链和目标 WebHandler 抛出的异常。当使用 WebFlux 配置 时,注册 WebExceptionHandler 就像将其声明为 Spring bean 并(可选)通过在 bean 声明上使用 @Order 或实现 Ordered 来表达优先级一样简单。

下表描述了可用的 WebExceptionHandler 实现:

异常处理程序 描述

ResponseStatusExceptionHandler

提供对 ResponseStatusException 类型异常的处理, 通过将响应设置为异常的 HTTP 状态码。

WebFluxResponseStatusExceptionHandler

ResponseStatusExceptionHandler 的扩展,它还可以确定任何异常上的 @ResponseStatus 注解的 HTTP 状态码。

此处理程序在 WebFlux 配置 中声明。

编解码器

spring-webspring-core 模块通过使用 Reactive Streams 背压的非阻塞 I/O,提供对字节内容与高级对象之间序列化和反序列化的支持。以下描述了此支持:

  • EncoderDecoder 是独立于 HTTP 对内容进行编码和解码的低级契约。

  • HttpMessageReaderHttpMessageWriter 是用于编码和解码 HTTP 消息内容的契约。

  • Encoder 可以用 EncoderHttpMessageWriter 包装以使其适用于 Web 应用程序,而 Decoder 可以用 DecoderHttpMessageReader 包装。

  • DataBuffer 抽象了不同的 字节缓冲区表示(例如,Netty ByteBufjava.nio.ByteBuffer 等),并且是 所有编解码器都处理的对象。有关此主题的更多信息,请参阅 "Spring Core" 部分中的 数据缓冲区和编解码器

spring-core 模块提供 byte[]ByteBufferDataBufferResourceString 编码器和解码器实现。spring-web 模块提供 Jackson JSON、Jackson Smile、JAXB2、Protocol Buffers 和其他编码器和解码器,以及 用于表单数据、多部分内容、服务器发送事件等的仅 Web HTTP 消息读写器实现。

ClientCodecConfigurerServerCodecConfigurer 通常用于配置和 自定义应用程序中使用的编解码器。请参阅配置 HTTP 消息编解码器的部分。

Jackson JSON

当存在 Jackson 库时,JSON 和二进制 JSON (Smile) 都受支持。

Jackson2Decoder 的工作方式如下:

  • Jackson 的异步、非阻塞解析器用于将字节块流聚合成 代表 JSON 对象的 TokenBuffer

  • 每个 TokenBuffer 都传递给 Jackson 的 ObjectMapper 以创建更高级别的对象。

  • 当解码为单值发布者(例如 Mono)时,有一个 TokenBuffer

  • 当解码为多值发布者(例如 Flux)时,一旦收到足够字节形成完整的对象,每个 TokenBuffer 就会传递给 ObjectMapper。输入内容可以是 JSON 数组,或任何 行分隔 JSON 格式,例如 NDJSON、 JSON Lines 或 JSON Text Sequences。

Jackson2Encoder 的工作方式如下:

  • 对于单值发布者(例如 Mono),只需通过 ObjectMapper 对其进行序列化。

  • 对于 application/json 的多值发布者,默认情况下使用 Flux#collectToList() 收集值,然后序列化生成的集合。

  • 对于具有流媒体类型(例如 application/x-ndjsonapplication/stream+x-jackson-smile)的多值发布者, 使用 行分隔 JSON 格式单独编码、写入和 刷新每个值。其他流媒体类型可以向编码器注册。

  • 对于 SSE,Jackson2Encoder 会针对每个事件调用,并刷新输出以确保 无延迟交付。

默认情况下,Jackson2EncoderJackson2Decoder 都不支持 String 类型元素。相反,默认假设是字符串或字符串序列表示序列化的 JSON 内容,由 CharSequenceEncoder 渲染。如果您需要从 Flux<String> 渲染 JSON 数组,请使用 Flux#collectToList() 并编码 Mono<List<String>>

表单数据

FormHttpMessageReaderFormHttpMessageWriter 支持解码和编码 application/x-www-form-urlencoded 内容。

在服务器端,表单内容通常需要从多个地方访问, ServerWebExchange 提供了一个专用的 getFormData() 方法,它通过 FormHttpMessageReader 解析内容,然后缓存结果以供重复访问。 参见 WebHandler API 部分中的 表单数据

一旦使用了 getFormData(),就无法再从请求体中读取原始原始内容。 因此,应用程序应始终通过 ServerWebExchange 访问缓存的表单数据, 而不是从原始请求体中读取。

多部分

MultipartHttpMessageReaderMultipartHttpMessageWriter 支持解码和 编码 "multipart/form-data"、"multipart/mixed" 和 "multipart/related" 内容。 MultipartHttpMessageReader 又委托给另一个 HttpMessageReader 进行实际解析为 Flux<Part>,然后简单地将这些部分收集到 MultiValueMap 中。 默认情况下,使用 DefaultPartHttpMessageReader,但这可以通过 ServerCodecConfigurer 进行更改。 有关 DefaultPartHttpMessageReader 的更多信息,请参阅 DefaultPartHttpMessageReader 的 Javadoc

在服务器端,多部分表单内容可能需要从多个位置访问, ServerWebExchange 提供了一个专用的 getMultipartData() 方法,该方法通过 MultipartHttpMessageReader 解析内容,然后缓存结果以供重复访问。 请参阅 WebHandler API 部分中的 多部分数据

一旦使用了 getMultipartData(),就不能再从请求体中读取原始原始内容。 因此,应用程序必须始终使用 getMultipartData() 进行重复的、类似映射的部分访问, 或者依赖 SynchronossPartHttpMessageReader 进行一次性 Flux<Part> 访问。

Protocol Buffers

ProtobufEncoderProtobufDecoder 支持对 com.google.protobuf.Message 类型进行 "application/x-protobuf"、"application/octet-stream" 和 "application/vnd.google.protobuf" 内容的解码和编码。如果内容是与内容类型(如 "application/x-protobuf;delimited=true")一起接收/发送的 "delimited" 参数,它们还支持值流。 这需要 "com.google.protobuf:protobuf-java" 库,版本 3.29 及更高版本。

ProtobufJsonDecoderProtobufJsonEncoder 变体支持读取和写入 JSON 文档到 Protobuf 消息。 它们需要 "com.google.protobuf:protobuf-java-util" 依赖项。请注意,JSON 变体不支持读取消息流, 有关更多详细信息,请参阅 ProtobufJsonDecoder 的 Javadoc

限制

DecoderHttpMessageReader 实现,如果它们缓冲部分或全部输入流,则可以配置内存中最大缓冲字节数的限制。 在某些情况下,缓冲发生是因为输入被聚合并表示为单个对象——例如,带有 @RequestBody byte[] 的控制器方法、 x-www-form-urlencoded 数据等。缓冲也可能发生在流式传输中,当分割输入流时——例如,分隔文本、JSON 对象流等。 对于这些流式传输情况,限制适用于流中一个对象关联的字节数。

要配置缓冲区大小,您可以检查给定的 DecoderHttpMessageReader 是否公开了 maxInMemorySize 属性,如果有,Javadoc 将详细说明默认值。在服务器端,ServerCodecConfigurer 提供了一个单一的位置来设置所有编解码器,请参阅 HTTP 消息编解码器。在客户端,所有编解码器的限制可以在 WebClient.Builder 中更改。

对于 多部分解析maxInMemorySize 属性限制了非文件部分的 大小。对于文件部分,它决定了将部分写入磁盘的阈值。对于写入磁盘的文件部分,还有一个 maxDiskUsagePerPart 属性来限制每个部分占用的磁盘空间量。还有一个 maxParts 属性来限制多部分请求中的总部分数。 要在 WebFlux 中配置所有这三个属性,您需要向 ServerCodecConfigurer 提供一个预配置的 MultipartHttpMessageReader 实例。

流式传输

当流式传输到 HTTP 响应时(例如,text/event-streamapplication/x-ndjson),定期发送数据非常重要,以便 更快地可靠检测到断开连接的客户端。这种发送可以是 仅包含注释的空 SSE 事件,或任何其他实际上充当心跳的“无操作”数据。

DataBuffer

DataBuffer 是 WebFlux 中字节缓冲区的表示。本参考的 Spring Core 部分在 数据缓冲区和编解码器 部分对此有更多介绍。关键点在于,在某些 服务器(如 Netty)上,字节缓冲区是池化和引用计数的,必须在消耗后释放, 以避免内存泄漏。

WebFlux 应用程序通常不需要担心此类问题,除非它们 直接消耗或生成数据缓冲区,而不是依赖编解码器进行高级对象之间的转换, 或者除非它们选择创建自定义编解码器。对于此类情况,请查阅 数据缓冲区和编解码器 中的信息, 特别是 使用 DataBuffer 部分。

日志

Spring WebFlux 中的 DEBUG 级别日志旨在紧凑、最小且 对人类友好。它侧重于反复有用的高价值信息,而不是仅在调试特定问题时有用的信息。

TRACE 级别日志通常遵循与 DEBUG 相同的原则(例如,它也不应该像消防水带一样),但可用于调试任何问题。此外,一些日志消息在 TRACEDEBUG 级别可能会显示不同程度的详细信息。

良好的日志记录来自使用日志的经验。如果您发现任何不符合所述目标的内容,请告诉我们。

日志 ID

在 WebFlux 中,单个请求可以在多个线程上运行,线程 ID 对于关联属于特定请求的日志消息没有用。这就是为什么 WebFlux 日志消息默认以请求特定的 ID 为前缀。

在服务器端,日志 ID 存储在 ServerWebExchange 属性中 (LOG_ID_ATTRIBUTE), 而基于该 ID 的完全格式化前缀可从 ServerWebExchange#getLogPrefix() 获取。在 WebClient 端,日志 ID 存储在 ClientRequest 属性中 (LOG_ID_ATTRIBUTE) ,而完全格式化的前缀可从 ClientRequest#logPrefix() 获取。

敏感数据

DEBUGTRACE 日志可能会记录敏感信息。这就是为什么表单参数和 标头默认被屏蔽,您必须明确启用它们的完整日志记录。

以下示例展示了如何为服务器端请求执行此操作:

  • Java

  • Kotlin

@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {

	@Override
	public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
		configurer.defaultCodecs().enableLoggingRequestDetails(true);
	}
}
@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {

	override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
		configurer.defaultCodecs().enableLoggingRequestDetails(true)
	}
}

以下示例展示了如何为客户端请求执行此操作:

  • Java

  • Kotlin

Consumer<ClientCodecConfigurer> consumer = configurer ->
		configurer.defaultCodecs().enableLoggingRequestDetails(true);

WebClient webClient = WebClient.builder()
		.exchangeStrategies(strategies -> strategies.codecs(consumer))
		.build();
val consumer: (ClientCodecConfigurer) -> Unit  = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }

val webClient = WebClient.builder()
		.exchangeStrategies({ strategies -> strategies.codecs(consumer) })
		.build()

Appenders

SLF4J 和 Log4J 2 等日志库提供异步日志记录器,可避免阻塞。虽然这些日志记录器有其自身的缺点,例如可能会丢失无法排队进行日志记录的消息,但它们是目前在响应式、非阻塞应用程序中使用的最佳可用选项。

自定义编解码器

应用程序可以注册自定义编解码器,以支持默认编解码器不支持的额外媒体类型或特定行为。

开发人员表达的一些配置选项会在默认编解码器上强制执行。 自定义编解码器可能希望有机会与这些偏好保持一致, 例如 强制执行缓冲限制记录敏感数据

以下示例展示了如何为客户端请求执行此操作:

  • Java

  • Kotlin

WebClient webClient = WebClient.builder()
		.codecs(configurer -> {
			CustomDecoder decoder = new CustomDecoder();
			configurer.customCodecs().registerWithDefaultConfig(decoder);
		})
		.build();
val webClient = WebClient.builder()
		.codecs({ configurer ->
			val decoder = CustomDecoder()
			configurer.customCodecs().registerWithDefaultConfig(decoder)
		 })
		.build()