|
对于最新的稳定版本,请使用 Spring Framework 7.0.6! |
Reactive Core
The spring-web 模块包含以下用于响应式 Web 应用程序的基础支持:
-
对于服务器请求处理,有两种级别的支持。
-
Http处理器:用于处理HTTP请求的基本契约,支持非阻塞I/O和响应式流的背压机制,同时提供了Reactor Netty、Undertow、Tomcat、Jetty以及任何Servlet容器的适配器。
-
WebHandlerAPI: 稍高层次、通用目的的Web API,用于处理请求,在其上构建了具体的编程模型,如注解控制器和函数端点。
-
-
对于客户端,有一个基本的
ClientHttpConnector合同用于执行非阻塞 I/O 和反应流背压的 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支持读写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 | 构件名称 |
|---|---|---|
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。
WebHandler 应用程序接口
The org.springframework.web.server 包基于 HttpHandler 合同
提供一个通用的Web API,用于通过多个
WebExceptionHandler、多个
WebFilter 和一个单一的
WebHandler 组件处理请求。该链可以通过 WebHttpHandlerBuilder 简单地指向一个 Spring
ApplicationContext 来组合,在那里组件是
自动检测 的,并/或通过向构建器注册组件。
While HttpHandler 有一个简单的目标,即抽象不同 HTTP 服务器的使用,WebHandler API 旨在提供一组在 Web 应用程序中常用的功能,例如:
-
用户会话及其属性。
-
请求属性。
-
Resolved
Locale或Principal用于请求。 -
访问解析和缓存的表单数据。
-
多部分数据的抽象。
-
和更多...
特殊bean类型
以下表格列出了WebHttpHandlerBuilder可以在Spring ApplicationContext中自动检测的组件,或者可以直接注册到其中的组件:
| 豆名称 | Beans类型 | 计数 | 描述 |
|---|---|---|---|
任何 |
|
0..N |
为来自 |
任何 |
|
0..N |
将拦截样式逻辑应用于过滤器链的其余部分和目标 |
|
|
1 |
处理请求的处理器。 |
|
|
0..1 |
管理器通过 |
|
|
0..1 |
对于访问 |
|
|
0..1 |
The resolver for |
|
|
0..1 |
对于转发类型标头的处理,可以通过提取并删除它们或仅删除它们来实现。默认情况下不使用。 |
表单数据
ServerWebExchange 提供了以下方法来访问表单数据:
-
Java
-
Kotlin
Mono<MultiValueMap<String, String>> getFormData();
suspend fun getFormData(): MultiValueMap<String, String>
The 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-data、
multipart/mixed和multipart/related内容解析为MultiValueMap。
默认情况下,这是DefaultPartHttpMessageReader,它没有任何第三方依赖。
或者,可以使用SynchronossPartHttpMessageReader,它基于
Synchronoss NIO Multipart库。
两者都通过ServerCodecConfigurer bean进行配置
(请参阅Web处理程序API)。
要以流式处理方式解析多部分数据,您可以使用Flux<PartEvent>(从PartEventHttpMessageReader返回)而非@RequestPart,
因为后者意味着需要像Map那样按名称访问各个部分,
从而要求完整解析多部分数据。相比之下,使用@RequestBody可将内容解码为Flux<PartEvent>,
而无需收集到MultiValueMap中。
请求头转发
当请求通过代理(如负载均衡器)时,主机、端口和协议可能会发生变化。这使得从客户端的角度来看,创建指向正确主机、端口和协议的链接变得具有挑战性。
RFC 7239 定义了 Forwarded HTTP 标头,代理可以使用该标头提供有关原始请求的信息。还有其他非标准标头,包括 X-Forwarded-Host、X-Forwarded-Port、X-Forwarded-Proto、X-Forwarded-Ssl 和 X-Forwarded-Prefix。
ForwardedHeaderTransformer 是一个组件,它根据请求头转发修改请求的主机、端口和协议,然后删除这些标头。如果你将其声明为名称为 forwardedHeaderTransformer 的 bean,它将被
检测到 并使用。
转发头可能存在安全问题,因为应用程序无法知道这些头是被代理按预期添加的,还是被恶意客户端添加的。这就是为什么在信任边界的代理应该配置为移除来自外部的不可信转发流量。你也可以配置ForwardedHeaderTransformer与removeOnly=true,在这种情况下,它会移除但不使用这些头。
在5.1中,ForwardedHeaderFilter已被弃用并由ForwardedHeaderTransformer取代,因此可以在创建交换之前更早地处理转发的头信息。如果已配置了该过滤器,它将从过滤器列表中移除,并改用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实现:
| 异常处理 | 描述 |
|---|---|
|
为类型为
|
|
Extension of 此处理器在WebFlux Config中声明。 |
编解码器
The spring-web 和 spring-core 模块提供了通过非阻塞 I/O 与 Reactive Streams 背压进行序列化和反序列化字节内容到高级对象的支持。以下描述了这种支持:
-
HttpMessageReader和HttpMessageWriter是用于编码和解码 HTTP 消息内容的合同。 -
一个
Encoder可以用EncoderHttpMessageWriter包装,以便在 Web 应用程序中使用,而一个Decoder可以用DecoderHttpMessageReader包装。 -
DataBuffer抽象了不同的字节缓冲区表示(例如 NettyByteBuf,java.nio.ByteBuffer等),并且是所有编解码器工作的基础。有关此主题的更多信息,请参见“Spring 核心”部分中的数据缓冲区和编解码器。
The spring-core 模块提供了 byte[]、ByteBuffer、DataBuffer、Resource、和
String 编码器和解码器实现。The spring-web 模块提供了 Jackson
JSON、Jackson Smile、JAXB2、Protocol Buffers 以及其他编码器和解码器,同时还提供了
仅限 Web 的 HTTP 消息读取器和写入器实现,用于表单数据、多部分内容、服务器发送的事件等。
ClientCodecConfigurer 和 ServerCodecConfigurer 通常用于配置和
自定义要在应用程序中使用的编解码器。请参见有关配置
HTTP 消息编解码器的部分。
Jackson JSON
JSON 和二进制 JSON (Smile) 都在 Jackson 库存在时得到支持。
The Jackson2Decoder works as follows:
-
Jackson的异步、非阻塞解析器用于聚合字节块流 成
TokenBuffer,每个代表一个JSON对象。 -
每个
TokenBuffer都被传递给 Jackson 的ObjectMapper以创建一个更高级的对象。 -
当解码为单值发布者(例如
Mono)时,有一个TokenBuffer。 -
在解码为多值发布者(例如
Flux)时,每个TokenBuffer会在接收到足够字节以形成完整对象时立即传递给ObjectMapper。输入内容可以是 JSON 数组,或者任何 行分隔的 JSON 格式,如 NDJSON、JSON Lines 或 JSON 文本序列。
The Jackson2Encoder works as follows:
-
对于单值发布者(例如
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来访问缓存的表单数据,而不是从原始请求体中读取。
多部分
MultipartHttpMessageReader和MultipartHttpMessageWriter支持对“multipart/form-data”、“multipart/mixed”和“multipart/related”内容进行解码和编码。
接着MultipartHttpMessageReader委托另一个HttpMessageReader将内容实际解析为Flux<Part>,然后简单地将各个部分收集到MultiValueMap中。
默认情况下,使用DefaultPartHttpMessageReader,但可以通过ServerCodecConfigurer更改。
有关DefaultPartHttpMessageReader的更多信息,请参阅DefaultPartHttpMessageReader的javadoc。
在服务器端,当可能需要从多个地方访问multipart表单内容时,ServerWebExchange 提供了一个专门的 getMultipartData() 方法,该方法通过 MultipartHttpMessageReader 解析内容,然后缓存结果以供重复访问。请参见 Multipart Data 在 WebHandler API 部分。
一旦使用了getMultipartData(),就无法再从请求体中读取原始的原始内容。因此,应用程序必须始终使用getMultipartData()来重复、类似映射地访问部分,或者依赖于SynchronossPartHttpMessageReader进行一次性访问Flux<Part>。
限制
Decoder 和 HttpMessageReader 实现可以配置为在内存中缓冲部分或全部输入流,并且可以设置最大缓冲字节数的限制。
在某些情况下,缓冲发生是因为输入被聚合并表示为单个对象——例如,具有 @RequestBody byte[]、x-www-form-urlencoded 数据等的控制器方法。缓冲也可能发生在流式处理时,例如分割输入流——例如,分隔文本、JSON 对象流等。对于这些流式处理情况,限制适用于流中一个对象关联的字节数。
要配置缓冲区大小,您可以检查给定的Decoder或HttpMessageReader是否暴露了maxInMemorySize属性,如果暴露了,Javadoc将提供有关默认值的详细信息。在服务器端,ServerCodecConfigurer提供了一个设置所有编解码器的单一位置,详情请参见HTTP消息编解码器。在客户端,可以在WebClient.Builder中更改所有编解码器的限制。
对于 多部分解析,maxInMemorySize 属性限制了非文件部分的大小。对于文件部分,它确定了将部分写入磁盘的阈值。对于写入磁盘的文件部分,还有一个 maxDiskUsagePerPart 属性来限制每个部分的磁盘空间量。还有一个 maxParts 属性来限制multipart请求中的部分总数。要在WebFlux中配置这三个属性,您需要向 ServerCodecConfigurer 提供一个预配置的 MultipartHttpMessageReader 实例。
流式传输
当向HTTP响应流式传输数据(例如,text/event-stream, application/x-ndjson)时,定期发送数据非常重要,以便能够尽早可靠地检测到已断开连接的客户端。这种发送可以是一个仅包含注释的空SSE事件或任何其他“无操作”数据,这些数据实际上可以作为心跳。
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()