|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
响应式核心
spring-web 模块包含以下对响应式 Web 应用程序的基础支持:
-
对于服务器请求处理,有两个级别的支持。
-
HttpHandler:用于处理 HTTP 请求的基础契约,支持非阻塞 I/O 和响应式流(Reactive Streams)背压机制,并提供适配器以兼容 Reactor Netty、Undertow、Tomcat、Jetty 以及任意 Servlet 容器。
-
WebHandlerAPI:更高级别的通用 Web API,用于请求处理,基于此构建了注解控制器和函数式端点等具体编程模型。
-
-
在客户端方面,有一个基本的
ClientHttpConnector契约,用于通过非阻塞 I/O 和响应式流(Reactive Streams)背压机制执行 HTTP 请求,同时还提供了针对 Reactor Netty、响应式的 Jetty HttpClient 以及 Apache HttpComponents 的适配器。 应用程序中使用的高层级 WebClient 正是构建于这一基本契约之上。 -
对于客户端和服务器,用于HTTP请求和响应内容的序列化与反序列化的编解码器。
HttpHandler
HttpHandler 是一个简单的契约,仅包含一个用于处理请求和响应的方法。它被有意设计得极为精简,其主要且唯一的目的就是对不同的 HTTP 服务器 API 提供一个最小化的抽象。
下表描述了所支持的服务器 API:
| 服务器名称 | 所使用的服务器 API | 响应式流支持 |
|---|---|---|
Netty |
Netty API |
|
Undertow |
Undertow API |
spring-web:Undertow 到响应式流(Reactive Streams)的桥接 |
Tomcat |
Servlet 非阻塞 I/O;Tomcat API 用于读写 ByteBuffer 与 byte[] |
spring-web:Servlet 非阻塞 I/O 到响应式流(Reactive Streams)的桥接 |
Jetty |
Servlet 非阻塞 I/O;Jetty API 写入 ByteBuffer 与 byte[] 的对比 |
spring-web:Servlet 非阻塞 I/O 到响应式流(Reactive Streams)的桥接 |
Servlet 容器 |
Servlet 非阻塞 I/O |
spring-web:Servlet 非阻塞 I/O 到响应式流(Reactive Streams)的桥接 |
下表描述了服务器依赖项(另请参阅支持的版本):
| 服务器名称 | 组 ID | 构件名称 |
|---|---|---|
Reactor Netty |
io.projectreactor.netty |
reactor-netty |
Undertow |
io.undertow |
undertow-core |
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()
Undertow
-
Java
-
Kotlin
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
val handler: HttpHandler = ...
val adapter = UndertowHttpHandlerAdapter(handler)
val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build()
server.start()
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 = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);
Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();
ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();
val handler: HttpHandler = ...
val servlet = JettyHttpHandlerAdapter(handler)
val server = Server()
val contextHandler = ServletContextHandler(server, "")
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();
val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()
Servlet 容器
若要作为 WAR 部署到任何 Servlet 容器,您可以扩展并包含
AbstractReactiveWebInitializer
到 WAR 中。该类使用 ServletHttpHandlerAdapter 包装一个 HttpHandler,
并将其注册为 Servlet。
WebHandlerAPI
org.springframework.web.server 包基于 HttpHandler 契约构建,提供了一个通用的 Web API,用于通过由多个 WebExceptionHandler、多个 WebFilter 以及单个 WebHandler 组件组成的链来处理请求。该链可以通过 WebHttpHandlerBuilder 进行组装,只需指向一个 Spring ApplicationContext(其中的组件会被 自动检测),和/或通过向构建器注册组件来完成。
虽然 HttpHandler 的目标很简单,即抽象化不同 HTTP 服务器的使用,但
WebHandler API 旨在提供更广泛的功能集,这些功能通常用于 Web 应用程序中,例如:
-
带有属性的用户会话。
-
请求属性。
-
为请求解析出的
Locale或Principal。 -
对已解析并缓存的表单数据的访问。
-
多部分数据的抽象。
-
以及更多……
特殊 Bean 类型
下表列出了 WebHttpHandlerBuilder 可以在 Spring ApplicationContext 中自动检测到的组件,或者可以直接向其注册的组件:
| Bean 名称 | Bean 类型 | 计数 | <description> </description> |
|---|---|---|---|
<any> |
|
0..N |
为来自 |
<any> |
|
0..N |
将拦截风格的逻辑应用于过滤器链其余部分以及目标 |
|
|
1 |
请求的处理器。 |
|
|
0..1 |
通过 |
|
|
0..1 |
用于访问 |
|
|
0..1 |
通过 |
|
|
0..1 |
用于处理转发类型(forwarded type)的头部信息,可以提取并移除这些头部,也可以仅移除它们。 默认情况下不使用。 |
表单数据
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 提供了以下方法用于访问多部分(multipart)数据:
-
Java
-
Kotlin
Mono<MultiValueMap<String, Part>> getMultipartData();
suspend fun getMultipartData(): MultiValueMap<String, Part>
DefaultServerWebExchange 使用配置的 HttpMessageReader<MultiValueMap<String, Part>> 将 multipart/form-data、multipart/mixed 和 multipart/related 内容解析为 MultiValueMap。
默认情况下,这是 DefaultPartHttpMessageReader,它没有任何第三方依赖项。
或者,也可以使用基于 Synchronoss NIO Multipart 库的 SynchronossPartHttpMessageReader。
两者均通过 ServerCodecConfigurer Bean 进行配置(请参阅 Web Handler API)。
要以流式方式解析 multipart 数据,您可以使用 Flux<PartEvent> 返回的 PartEventHttpMessageReader,而不是使用 @RequestPart。因为 Map 意味着通过名称对各个部分进行类似 @RequestBody 的访问,因此需要完整地解析整个 multipart 数据。
相比之下,您可以使用 Flux<PartEvent> 将内容解码为 MultiValueMap,而无需将其收集到 7 中。
转发的请求头
当请求经过代理(例如负载均衡器)时,主机、端口和协议方案可能会发生变化。这从客户端的角度来看,使得创建指向正确主机、端口和协议方案的链接变得具有挑战性。
RFC 7239 定义了 Forwarded HTTP 头部,
代理可以使用该头部提供有关原始请求的信息。此外,还存在其他
非标准的头部,包括 X-Forwarded-Host、X-Forwarded-Port、
X-Forwarded-Proto、X-Forwarded-Ssl 和 X-Forwarded-Prefix。
ForwardedHeaderTransformer 是一个组件,它根据转发头(forwarded headers)修改请求的主机、端口和协议(scheme),然后移除这些头信息。如果你将其声明为名为 forwardedHeaderTransformer 的 Bean,它将被自动检测并使用。
转发头(forwarded headers)存在安全方面的考虑,因为应用程序无法判断这些头是由代理按预期添加的,还是由恶意客户端伪造的。因此,在信任边界处的代理应配置为移除来自外部的不可信转发流量。你也可以将 ForwardedHeaderTransformer 配置为 removeOnly=true,在这种情况下,它会移除这些头但不会使用它们。
在 5.1 版本中,ForwardedHeaderFilter 已被弃用,并由 ForwardedHeaderTransformer 取代,以便在创建 exchange 之前更早地处理转发头信息。如果仍然配置了该过滤器,它将从过滤器列表中移除,并改用 ForwardedHeaderTransformer。 |
过滤器
在 WebHandler API 中,您可以使用 WebFilter 在过滤器链的其余部分和目标 WebHandler 处理之前和之后应用拦截式逻辑。当使用 WebFlux 配置 时,注册 WebFilter 非常简单,只需将其声明为 Spring Bean,并(可选地)通过在 Bean 声明上使用 @Order 或实现 Ordered 来表达优先级。
跨域资源共享(CORS)
Spring WebFlux 通过控制器上的注解提供了细粒度的 CORS 配置支持。然而,当你将其与 Spring Security 一起使用时,我们建议依赖内置的 CorsFilter,该过滤器必须排在 Spring Security 过滤器链之前。
请参阅关于 CORS 的章节以及 CORS WebFilter 以获取更多详细信息。
异常
在 WebHandler API 中,您可以使用 WebExceptionHandler 来处理来自 WebFilter 实例链和目标 WebHandler 的异常。当使用 WebFlux 配置 时,注册 WebExceptionHandler 非常简单,只需将其声明为 Spring Bean,并(可选地)通过在 Bean 声明上使用 @Order 或实现 Ordered 来表达优先级。
下表描述了可用的 WebExceptionHandler 实现:
| 异常处理器 | <description> </description> |
|---|---|
|
提供对类型为
|
|
此处理器在WebFlux 配置中声明。 |
编解码器
spring-web 和 spring-core 模块通过带有响应式流(Reactive Streams)背压机制的非阻塞 I/O,提供了将字节内容与高层对象之间进行序列化和反序列化的支持。以下内容描述了该支持:
-
HttpMessageReader和HttpMessageWriter是用于编码和解码 HTTP 消息内容的契约。 -
可以使用
Encoder包装一个EncoderHttpMessageWriter,以使其适用于 Web 应用程序;而Decoder则可以使用DecoderHttpMessageReader进行包装。 -
DataBuffer抽象了不同的字节缓冲区表示形式(例如 NettyByteBuf、java.nio.ByteBuffer等),并且是所有编解码器操作的基础。有关此主题的更多信息,请参阅“Spring Core”部分中的 数据缓冲区和编解码器。
spring-core 模块提供了 byte[]、ByteBuffer、DataBuffer、Resource 和
String 的编码器和解码器实现。spring-web 模块则提供了 Jackson JSON、Jackson Smile、JAXB2、Protocol Buffers 等编码器和解码器,以及仅用于 Web 的 HTTP 消息读取器和写入器实现,用于处理表单数据、多部分(multipart)内容、服务器发送事件(server-sent events)等。
ClientCodecConfigurer 和 ServerCodecConfigurer 通常用于配置和自定义应用程序中使用的编解码器。请参阅有关配置HTTP 消息编解码器的部分。
Jackson JSON
当 Jackson 库存在时,JSON 和二进制 JSON(Smile)均受支持。
Jackson2Decoder 的工作方式如下:
-
Jackson 的异步、非阻塞解析器用于将字节块流聚合为多个
TokenBuffer,每个1代表一个 JSON 对象。 -
每个
TokenBuffer都会被传递给 Jackson 的ObjectMapper,以创建一个更高层次的对象。 -
当解码为单值发布者(例如
Mono)时,只有一个TokenBuffer。 -
在解码为多值发布者(例如
Flux)时,一旦接收到足够构成一个完整对象的字节,每个TokenBuffer就会立即传递给ObjectMapper。输入内容可以是一个 JSON 数组,也可以是任意 行分隔的 JSON 格式,例如 NDJSON、 JSON Lines 或 JSON 文本序列。
Jackson2Encoder 的工作方式如下:
-
对于单值发布者(例如
Mono),只需通过ObjectMapper将其序列化即可。 -
对于具有
application/json类型的多值发布者,默认使用Flux#collectToList()收集所有值,然后对生成的集合进行序列化。 -
对于具有流式媒体类型(例如
application/x-ndjson或application/stream+x-jackson-smile)的多值发布者,请使用行分隔 JSON 格式对每个值分别进行编码、写入和刷新。其他流式媒体类型也可以向编码器注册。 -
对于 SSE,每次事件都会调用
Jackson2Encoder,并且会刷新输出以确保及时传递。
|
默认情况下, |
表单数据
FormHttpMessageReader 和 FormHttpMessageWriter 支持解码和编码
application/x-www-form-urlencoded 内容。
在服务器端,当需要从多个位置访问表单内容时,
ServerWebExchange 提供了一个专用的 getFormData() 方法,该方法通过 FormHttpMessageReader 解析内容,
然后缓存结果以便重复访问。
请参阅 表单数据,位于 WebHandler API 部分。
一旦使用了 getFormData(),就无法再从请求体中读取原始的原始内容。因此,应用程序应始终通过 ServerWebExchange 来访问缓存的表单数据,而不是直接读取原始请求体。
多部分(Multipart)
MultipartHttpMessageReader 和 MultipartHttpMessageWriter 支持解码和编码"multipart/form-data"、"multipart/mixed"以及"multipart/related"内容。
随后,MultipartHttpMessageReader 将实际解析工作委托给另一个 HttpMessageReader,将其解析为 Flux<Part>,然后简单地将各部分收集到 MultiValueMap 中。
默认情况下,使用的是 DefaultPartHttpMessageReader,但可以通过 ServerCodecConfigurer 进行更改。
有关 DefaultPartHttpMessageReader 的更多信息,请参阅
DefaultPartHttpMessageReader 的 Javadoc。
在服务器端,当需要从多个位置访问多部分表单内容时,ServerWebExchange 提供了专用的 getMultipartData() 方法,该方法通过 MultipartHttpMessageReader 解析内容,然后缓存结果以供重复访问。
请参阅 WebHandler API 章节中的 多部分数据。
一旦使用了 getMultipartData(),就无法再从请求体中读取原始的原始内容。因此,应用程序必须始终一致地使用 getMultipartData() 来实现对各部分(parts)的重复、类似 Map 的访问;否则,应依赖 SynchronossPartHttpMessageReader 对 Flux<Part> 进行一次性访问。
限制
可以为那些对部分或全部输入流进行缓冲的 Decoder 和 HttpMessageReader 实现配置一个内存中缓冲字节数的最大限制。
在某些情况下,缓冲的发生是因为输入被聚合并表示为单个对象——例如,带有 @RequestBody byte[] 的控制器方法、x-www-form-urlencoded 数据等。
在流式处理场景中,当对输入流进行拆分时(例如,分隔符分隔的文本、JSON 对象流等),也会发生缓冲。
对于这些流式处理的情况,该限制适用于流中单个对象所关联的字节数。
要配置缓冲区大小,您可以检查给定的 Decoder 或 HttpMessageReader 是否公开了 maxInMemorySize 属性,如果公开了该属性,其 Javadoc 中将包含有关默认值的详细信息。在服务器端,ServerCodecConfigurer 提供了一个统一的位置来设置所有编解码器,请参阅HTTP 消息编解码器。在客户端,所有编解码器的限制可以在WebClient.Builder 中进行修改。
对于多部分(Multipart)解析,maxInMemorySize 属性用于限制非文件部分的大小。对于文件部分,该属性决定了将该部分写入磁盘的阈值。对于写入磁盘的文件部分,还有一个额外的 maxDiskUsagePerPart 属性,用于限制每个部分所占用的磁盘空间。此外,还有一个 maxParts 属性,用于限制多部分请求中各部分的总数量。
在 WebFlux 中配置上述三个属性时,你需要向 MultipartHttpMessageReader 提供一个预先配置好的 ServerCodecConfigurer 实例。
流式处理
在向 HTTP 响应进行流式传输时(例如 text/event-stream、
application/x-ndjson),定期发送数据非常重要,以便能够更早而非更晚地可靠检测到客户端断开连接。这种发送可以是一个仅包含注释的空 SSE 事件,或者任何其他实际上可作为心跳机制的“无操作”(no-op)数据。
DataBuffer
DataBuffer 是 WebFlux 中字节缓冲区的表示形式。有关此内容的更多详细信息,请参阅本参考文档 Spring Core 部分中的数据缓冲区和编解码器一节。需要理解的关键点是,在某些服务器(如 Netty)上,字节缓冲区是经过池化并采用引用计数的,必须在使用完毕后予以释放,以避免内存泄漏。
WebFlux 应用程序通常无需关注此类问题,除非它们直接消费或生成数据缓冲区(而不是依赖编解码器在高层对象与数据之间进行转换),或者除非它们选择创建自定义编解码器。对于这些情况,请参阅数据缓冲区和编解码器中的相关信息,特别是使用 DataBuffer一节。
日志记录
DEBUG 级别的日志记录在 Spring WebFlux 中设计得简洁、精炼且对人类友好。它聚焦于那些反复有用的关键信息,而非仅在调试特定问题时才有用的其他信息。
TRACE 级别的日志记录通常遵循与 DEBUG 相同的原则(例如,也不应像“消防水带”一样输出大量日志),但可用于调试任何问题。此外,某些日志消息在 TRACE 级别下可能会比在 DEBUG 级别下显示更详细的细节。
良好的日志记录源于使用日志的经验。如果您发现任何不符合所述目标的内容,请告知我们。
日志 ID
在 WebFlux 中,单个请求可能会在多个线程上执行,因此线程 ID 对于关联属于特定请求的日志消息并无帮助。这就是为什么 WebFlux 的日志消息默认会加上一个请求专属的 ID 作为前缀。
在服务器端,日志 ID 存储在 ServerWebExchange 属性中
(LOG_ID_ATTRIBUTE),
而基于该 ID 的完整格式化前缀可从
ServerWebExchange#getLogPrefix() 获取。在 WebClient 端,日志 ID 存储在
ClientRequest 属性中
(LOG_ID_ATTRIBUTE)
,而完整的格式化前缀可从 ClientRequest#logPrefix() 获取。
敏感数据
DEBUG 和 TRACE 日志记录可能会记录敏感信息。因此,表单参数和请求头默认会被屏蔽,您必须显式启用才能完整记录它们。
以下示例展示了如何对服务器端请求进行此操作:
-
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()
追加器
像 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()