响应式核心
spring-web
模块包含对响应式Web应用程序的以下基础支持:
-
对于服务器请求处理,有两个级别的支持。
-
HttpHandler:具有非阻塞I/O和响应式流背压的HTTP请求处理的基本契约,以及适配器用于 Reactor Netty、Undertow、Tomcat、Jetty 和任何Servlet容器。
-
WebHandler
API:略高级别的通用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到响应式流的桥接 |
Tomcat |
Servlet非阻塞I/O;Tomcat API用于读取和写入ByteBuffer而不是byte[] |
spring-web:Servlet非阻塞I/O到响应式流的桥接 |
Jetty |
Servlet非阻塞I/O;Jetty API用于写入ByteBuffer而不是byte[] |
spring-web:Servlet非阻塞I/O到响应式流的桥接 |
Servlet容器 |
Servlet非阻塞I/O |
spring-web:Servlet非阻塞I/O到响应式流的桥接 |
以下表格描述了服务器依赖项(也请参阅支持的版本):
服务器名称 | 组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 |
以下代码片段展示了如何使用每个服务器API的HttpHandler
适配器:
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
API
org.springframework.web.server
包基于HttpHandler
协议构建,提供了一个通用的Web API,用于通过多个WebExceptionHandler
、多个WebFilter
和一个单一的WebHandler
组件来处理请求的链。可以通过简单地指向Spring ApplicationContext
来组装链,其中组件是自动检测的,和/或通过向构建器注册组件。
虽然HttpHandler
的简单目标是抽象出不同HTTP服务器的使用,但WebHandler
API旨在提供一组更广泛的功能,通常在Web应用程序中使用,例如:
-
具有属性的用户会话。
-
请求属性。
-
用于请求的已解析的
Locale
或Principal
。 -
访问已解析和缓存的表单数据。
-
多部分数据的抽象。
-
等等。
特殊的bean类型
下表列出了WebHttpHandlerBuilder
可以在Spring ApplicationContext中自动检测的组件,或者可以直接向其注册的组件:
Bean名称 | Bean类型 | 数量 | 描述 |
---|---|---|---|
<任意> |
|
0..N |
为 |
<任意> |
|
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
,它没有任何第三方依赖项。或者,可以使用基于Synchronoss NIO Multipart库的SynchronossPartHttpMessageReader
。这两者都通过ServerCodecConfigurer
bean进行配置(请参见Web Handler API)。
要以流式方式解析多部分数据,可以使用PartEventHttpMessageReader
返回的Flux<PartEvent>
,而不是使用@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
。
X-Forwarded-Host
虽然不是标准的,但 X-Forwarded-Host: <host>
是一个事实上的标准头部,用于将原始主机名传递给下游服务器。例如,如果将请求 example.com/resource
发送到一个代理,代理将请求转发到 localhost:8080/resource
,那么可以发送一个头部 X-Forwarded-Host: example.com
来通知服务器原始主机名是 example.com
。
X-Forwarded-Port
虽然不是标准的,X-Forwarded-Port: <port>
是一个事实上的标准头部,用于将原始端口传递给下游服务器。例如,如果将请求 example.com/resource
发送到一个代理,代理将请求转发到 localhost:8080/resource
,那么可以发送一个头部 X-Forwarded-Port: 443
来通知服务器原始端口是 443
。
X-Forwarded-Proto
虽然不是标准的,X-Forwarded-Proto: (https|http)
是一个事实上的标准头部,用于将原始协议(例如 https / http)传递给下游服务器。例如,如果将请求 example.com/resource
发送到一个代理,代理将请求转发到 localhost:8080/resource
,那么可以发送一个头部 X-Forwarded-Proto: https
来通知服务器原始协议是 https
。
X-Forwarded-Ssl
虽然不是标准的,X-Forwarded-Ssl: (on|off)
是一个事实上的标准头部,用于将原始协议(例如 https / http)传递给下游服务器。例如,如果将请求 example.com/resource
发送到一个代理,代理将请求转发到 localhost:8080/resource
,那么可以发送一个头部 X-Forwarded-Ssl: on
来通知服务器原始协议是 https
。
X-Forwarded-Prefix
虽然不是标准的,X-Forwarded-Prefix: <prefix>
是一个事实上的标准头部,用于将原始 URL 路径前缀传递给下游服务器。
X-Forwarded-Prefix
的使用可能因部署场景而异,需要灵活处理以允许替换、删除或添加目标服务器的路径前缀。
场景 1:覆盖路径前缀
https://example.com/api/{path} -> http://localhost:8080/app1/{path}
前缀是捕获组 {path}
前的路径起始部分。对于代理而言,前缀是 /api
,而对于服务器而言,前缀是 /app1
。在这种情况下,代理可以发送 X-Forwarded-Prefix: /api
,以使原始前缀 /api
覆盖服务器前缀 /app1
。
场景 2:移除路径前缀
有时,应用程序可能希望移除前缀。例如,考虑以下代理到服务器的映射:
https://app1.example.com/{path} -> http://localhost:8080/app1/{path} https://app2.example.com/{path} -> http://localhost:8080/app2/{path}
代理没有前缀,而应用程序 app1
和 app2
分别具有路径前缀 /app1
和 /app2
。代理可以发送 X-Forwarded-Prefix:
,以使空前缀覆盖服务器前缀 /app1
和 /app2
。
这种部署场景的常见情况是,按生产应用服务器付费许可证,最好在同一服务器上部署多个应用程序以减少费用。另一个原因是在同一服务器上运行更多应用程序,以共享服务器运行所需的资源。 在这些场景中,应用程序需要一个非空的上下文根,因为同一服务器上有多个应用程序。但是,这不应该在公共 API 的 URL 路径中可见,应用程序可能使用不同的子域,提供以下好处:
|
场景 3:插入路径前缀
在其他情况下,可能需要在前缀前添加一个前缀。例如,考虑以下代理到服务器的映射:
https://example.com/api/app1/{path} -> http://localhost:8080/app1/{path}
在这种情况下,代理具有前缀 /api/app1
,服务器具有前缀 /app1
。代理可以发送 X-Forwarded-Prefix: /api/app1
,以使原始前缀 /api/app1
覆盖服务器前缀 /app1
。
ForwardedHeaderTransformer
ForwardedHeaderTransformer
是一个组件,根据转发的头部修改请求的主机、端口和协议,然后移除这些头部。如果将其声明为名为 forwardedHeaderTransformer
的 bean,它将被检测并使用。检测并使用。
在 5.1 版本中,ForwardedHeaderFilter 已被弃用,并被 ForwardedHeaderTransformer 取代,以便更早处理转发的头部,即在创建交换之前。如果仍然配置了过滤器,则会将其从过滤器列表中移除,并改为使用 ForwardedHeaderTransformer 。 |
Filters
In the WebHandler
API, you can use a WebFilter
to apply interception-style logic before and after the rest of the processing chain of filters and the target WebHandler
. When using the WebFlux Config, registering a WebFilter
is as simple as declaring it as a Spring bean and (optionally) expressing precedence by using @Order
on the bean declaration or by implementing Ordered
.
CORS
Spring WebFlux provides fine-grained support for CORS configuration through annotations on controllers. However, when you use it with Spring Security, we advise relying on the built-in CorsFilter
, which must be ordered ahead of Spring Security’s chain of filters.
See the section on CORS and the CORS WebFilter
for more details.
Exceptions
In the WebHandler
API, you can use a WebExceptionHandler
to handle exceptions from the chain of WebFilter
instances and the target WebHandler
. When using the WebFlux Config, registering a WebExceptionHandler
is as simple as declaring it as a Spring bean and (optionally) expressing precedence by using @Order
on the bean declaration or by implementing Ordered
.
The following table describes the available WebExceptionHandler
implementations:
Exception Handler | Description |
---|---|
|
Provides handling for exceptions of type |
|
Extension of This handler is declared in the WebFlux Config. |
编解码器
spring-web
和spring-core
模块通过使用Reactive Streams背压的非阻塞I/O支持将字节内容序列化和反序列化为更高级别对象。以下描述了这种支持:
-
HttpMessageReader
和HttpMessageWriter
是用于编码和解码HTTP消息内容的契约。 -
Encoder
可以通过EncoderHttpMessageWriter
进行包装,以使其适用于Web应用程序,而Decoder
可以通过DecoderHttpMessageReader
进行包装。 -
DataBuffer
抽象了不同的字节缓冲区表示(例如Netty的ByteBuf
,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的异步、非阻塞解析器将字节块流聚合为每个代表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-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()
来重复、类似映射地访问部分,否则依赖于
SynchronossPartHttpMessageReader
一次性访问
Flux<Part>
。
限制
Decoder
和
HttpMessageReader
实现会在内存中缓冲部分或全部输入流,可以配置最大缓冲字节数限制。在某些情况下,缓冲发生是因为输入被聚合并表示为单个对象,例如带有
@RequestBody byte[]
、
x-www-form-urlencoded
数据等的控制器方法。在流式传输时也可能发生缓冲,例如拆分输入流,例如分隔文本、一系列JSON对象等。对于这些流式传输情况,限制适用于流中一个对象关联的字节数。
Decoder
或
HttpMessageReader
是否公开了
maxInMemorySize
属性,如果是,Javadoc将详细介绍默认值。在服务器端,
ServerCodecConfigurer
提供了一个设置所有编解码器的单一位置,参见
HTTP消息编解码器。在客户端端,可以在
WebClient.Builder中更改所有编解码器的限制。
多部分解析,
maxInMemorySize
属性限制非文件部分的大小。对于写入磁盘的文件部分,它确定部分写入磁盘的阈值。对于写入磁盘的文件部分,还有一个
maxDiskUsagePerPart
属性,用于限制每个部分的磁盘空间量。还有一个
maxParts
属性,用于限制多部分请求中的总部分数。要在WebFlux中配置所有这三个属性,您需要向
ServerCodecConfigurer
提供一个预配置的
MultipartHttpMessageReader
实例。
流式
text/event-stream
、
application/x-ndjson
),定期发送数据非常重要,以便更早地可靠地检测到断开连接的客户端。这样的发送可以是仅注释、空SSE事件或任何其他有效作为心跳的“无操作”数据。
数据缓冲区
数据缓冲区
是WebFlux中字节缓冲区的表示。关于这一点,Spring Core部分的参考资料在数据缓冲区和编解码器部分有更多介绍。需要理解的关键点是,在一些服务器(如Netty)上,字节缓冲区是被池化和引用计数的,当消耗时必须释放,以避免内存泄漏。
WebFlux应用程序通常不需要关注这些问题,除非它们直接消耗或产生数据缓冲区,而不是依赖编解码器将其转换为更高级别的对象,或者除非它们选择创建自定义编解码器。对于这种情况,请查看数据缓冲区和编解码器中的信息,特别是关于使用数据缓冲区的部分。
日志记录
DEBUG
级别的Spring WebFlux日志记录旨在紧凑、最小化且用户友好。它侧重于高价值的信息位,这些信息一遍又一遍地有用,而其他信息只在调试特定问题时有用。
TRACE
级别的日志记录通常遵循与DEBUG
相同的原则(例如,也不应该是一个firehose),但可用于调试任何问题。此外,一些日志消息在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()
自定义编解码器
应用程序可以注册自定义编解码器以支持其他媒体类型或默认编解码器不支持的特定行为。
以下示例显示了如何为客户端请求执行此操作:
-
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()