响应式核心
spring-web
模块包含以下对响应式 Web 应用程序的基础支持:
-
对于服务器请求处理,有两层支持。
-
HttpHandler: HTTP 请求处理的基本契约,具有非阻塞 I/O 和 Reactive Streams 背压,以及 Reactor Netty、Undertow、Tomcat、Jetty 和任何 Servlet 容器的适配器。
-
WebHandler
API: 稍高层的通用 Web API,用于请求处理,在其之上构建了具体编程模型,例如注解控制器和函数式端点。
-
-
对于客户端,有一个基本的
ClientHttpConnector
契约,用于通过非阻塞 I/O 和 Reactive Streams 背压执行 HTTP 请求,以及 Reactor Netty、响应式 Jetty HttpClient 和 Apache HttpComponents 的适配器。应用程序中使用的更高级别的 WebClient 基于此基本契约构建。 -
对于客户端和服务器,用于 HTTP 请求和响应内容序列化和反序列化的 编解码器。
HttpHandler
HttpHandler 是一个简单的契约,只有一个方法来处理请求和响应。它有意地保持最小化,其主要也是唯一目的是作为不同 HTTP 服务器 API 的最小抽象。
下表描述了支持的服务器 API:
服务器名称 | 使用的服务器 API | Reactive Streams 支持 |
---|---|---|
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 桥接 |
下表描述了服务器依赖项(另请参阅 支持的版本):
服务器名称 | Group id | Artifact name |
---|---|---|
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 中。该类将 HttpHandler
包装在 ServletHttpHandlerAdapter
中,并将其注册为 Servlet
。
WebHandler
API
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 类型 | 计数 | 描述 |
---|---|---|---|
<any> |
|
0..N |
提供对 |
<any> |
|
0..N |
在过滤器链的其余部分和目标 |
|
|
1 |
请求的处理程序。 |
|
|
0..1 |
通过 |
|
|
0..1 |
用于访问 |
|
|
0..1 |
通过 |
|
|
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-data
、multipart/mixed
和 multipart/related
内容解析为 MultiValueMap
。默认情况下,这是 DefaultPartHttpMessageReader
,它没有任何第三方依赖项。
或者,可以使用 SynchronossPartHttpMessageReader
,它基于 Synchronoss NIO Multipart 库。两者都通过 ServerCodecConfigurer
bean 配置(请参阅 Web Handler API)。
要以流式方式解析多部分数据,您可以使用 PartEventHttpMessageReader
返回的 Flux<PartEvent>
,而不是使用 @RequestPart
,因为 @RequestPart
意味着通过名称对各个部分进行 Map
访问,因此需要完全解析多部分数据。
相比之下,您可以使用 @RequestBody
将内容解码为 Flux<PartEvent>
,而无需收集到 MultiValueMap
。
ForwardedHeaderTransformer
ForwardedHeaderTransformer
是一个组件,它根据转发头修改请求的主机、端口和方案,然后删除这些头。如果您将其声明为名为 forwardedHeaderTransformer
的 bean,它将被 检测并使用。
在 5.1 版本中, |
过滤器
在 WebHandler
API 中,您可以使用 WebFilter
在过滤器链和目标 WebHandler
的其余处理链之前和之后应用拦截式逻辑。使用 WebFlux Config 时,注册 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 Config 时,注册 WebExceptionHandler
就像将其声明为 Spring bean 并(可选地)通过在 bean 声明上使用 @Order
或实现 Ordered
来表达优先级一样简单。
下表描述了可用的 WebExceptionHandler
实现:
异常处理程序 | 描述 |
---|---|
|
提供对 |
|
此处理程序在 WebFlux Config 中声明。 |
编解码器
spring-web
和 spring-core
模块支持通过非阻塞 I/O 和 Reactive Streams 背压将字节内容序列化和反序列化为高级对象。以下描述了这种支持:
-
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 消息读取器和写入器实现。
ClientCodecConfigurer
和 ServerCodecConfigurer
通常用于配置和自定义应用程序中使用的编解码器。请参阅配置 HTTP 消息编解码器的部分。
Jackson JSON
当存在 Jackson 库时,JSON 和二进制 JSON (Smile) 都受支持。
Jackson2Decoder
的工作方式如下:
-
Jackson 的异步、非阻塞解析器用于将字节块流聚合成
TokenBuffer
,每个TokenBuffer
代表一个 JSON 对象。 -
每个
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-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。
在服务器端,多部分表单内容可能需要从多个地方访问,ServerWebExchange
提供了一个专用的 getMultipartData()
方法,该方法通过 MultipartHttpMessageReader
解析内容,然后缓存结果以供重复访问。
请参阅 WebHandler
API 部分中的 多部分数据。
一旦使用了 getMultipartData()
,就无法再从请求体中读取原始原始内容。因此,应用程序必须始终使用 getMultipartData()
进行重复的、类似 Map 的部分访问,否则依赖 SynchronossPartHttpMessageReader
进行对 Flux<Part>
的一次性访问。
Protocol Buffers
ProtobufEncoder
和 ProtobufDecoder
支持解码和编码 com.google.protobuf.Message
类型的 "application/x-protobuf"、"application/octet-stream" 和 "application/vnd.google.protobuf" 内容。如果内容是使用 "delimited" 参数和内容类型一起接收/发送的(例如 "application/x-protobuf;delimited=true"),它们也支持值流。这需要 "com.google.protobuf:protobuf-java" 库,版本 3.29 及更高。
ProtobufJsonDecoder
和 ProtobufJsonEncoder
变体支持读写 JSON 文档到 Protobuf 消息。它们需要 "com.google.protobuf:protobuf-java-util" 依赖项。请注意,JSON 变体不支持读取消息流,有关更多详细信息,请参阅 ProtobufJsonDecoder
的 javadoc。
限制
Decoder
和 HttpMessageReader
实现,如果它们缓冲部分或全部输入流,可以配置一个内存中最大缓冲字节数的限制。
在某些情况下,缓冲的发生是因为输入被聚合并表示为单个对象——例如,带有 @RequestBody byte[]
的控制器方法、x-www-form-urlencoded
数据等等。
缓冲也可以在流式传输时发生,当分割输入流时——例如,分隔文本、JSON 对象流等等。
对于这些流式传输情况,限制适用于流中一个对象关联的字节数。
要配置缓冲区大小,您可以检查给定的 Decoder
或 HttpMessageReader
是否公开了 maxInMemorySize
属性,如果是,其 Javadoc 将包含有关默认值的详细信息。在服务器端,ServerCodecConfigurer
提供了一个统一的地方来设置所有编解码器,请参阅 HTTP 消息编解码器。在客户端,所有编解码器的限制可以在 WebClient.Builder 中更改。
对于 多部分解析,maxInMemorySize
属性限制非文件部分的大小。对于文件部分,它决定了将部分写入磁盘的阈值。对于写入磁盘的文件部分,还有一个 maxDiskUsagePerPart
属性来限制每个部分的磁盘空间量。还有一个 maxParts
属性来限制多部分请求中的总部分数。要在 WebFlux 中配置所有三个,您需要向 ServerCodecConfigurer
提供一个预配置的 MultipartHttpMessageReader
实例。
流式传输
当流式传输到 HTTP 响应时(例如,text/event-stream
、application/x-ndjson
),定期发送数据非常重要,以便更早而不是更晚地可靠地检测到断开连接的客户端。这种发送可以是仅包含注释的空 SSE 事件,或任何其他“无操作”数据,它将有效地充当心跳。
DataBuffer
DataBuffer
是 WebFlux 中字节缓冲区的表示。此引用的 Spring Core 部分在 数据缓冲区和编解码器 部分中有更多相关内容。需要理解的关键点是,在某些服务器(如 Netty)上,字节缓冲区是池化和引用计数的,并且在消耗后必须释放以避免内存泄漏。
WebFlux 应用程序通常不需要担心此类问题,除非它们直接消耗或生成数据缓冲区,而不是依赖编解码器在高级对象之间进行转换,或者除非它们选择创建自定义编解码器。对于此类情况,请查阅 数据缓冲区和编解码器 中的信息,尤其是 使用 DataBuffer 部分。
日志记录
Spring WebFlux 中的 DEBUG
级别日志记录被设计为紧凑、最小且人性化。它侧重于反复有用的高价值信息,而不是仅在调试特定问题时有用的其他信息。
TRACE
级别日志记录通常遵循与 DEBUG
相同的原则(例如,它也不应该是信息洪流),但可用于调试任何问题。此外,某些日志消息在 TRACE
与 DEBUG
级别可能显示不同的详细程度。
良好的日志记录来自使用日志的经验。如果您发现任何不符合所述目标的地方,请告诉我们。
日志 ID
在 WebFlux 中,单个请求可以在多个线程上运行,线程 ID 对于关联属于特定请求的日志消息没有用。这就是为什么 WebFlux 日志消息默认以请求特定的 ID 为前缀。
在服务器端,日志 ID 存储在 ServerWebExchange
属性(https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/server/ServerWebExchange.html#LOG_ID_ATTRIBUTE[LOG_ID_ATTRIBUTE
])中,而基于该 ID 的完全格式化前缀可从 ServerWebExchange#getLogPrefix()
获取。在 WebClient
侧,日志 ID 存储在 ClientRequest
属性(https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/reactive/function/client/ClientRequest.html#LOG_ID_ATTRIBUTE[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()
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()