Reactive Core
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 | Reactor Netty |
Undertow | Undertow API | spring-web: Undertow 到 Reactive Streams 桥接 |
Tomcat | Servlet 非阻塞 I/O;Tomcat API 用于读取和写入 ByteBuffers vs byte[] | spring-web: Servlet 非阻塞 I/O 到 Reactive Streams 桥接 |
Jetty | Servlet 非阻塞 I/O;Jetty API 用于写入 ByteBuffers vs byte[] | spring-web: Servlet 非阻塞 I/O 到 Reactive Streams 桥接 |
Servlet container | Servlet 非阻塞 I/O | spring-web: Servlet 非阻塞 I/O 到 Reactive Streams 桥接 |
下表描述了服务器依赖关系(另请参见支持的版本):
服务器名称 | 组 id | Artifact 名称 |
---|---|---|
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 容器中,你可以在 WAR 中扩展并包含 AbstractReactiveWebInitializer。该类使用 ServletHttpHandlerAdapter
包装一个 HttpHandler
并将其注册为一个 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> | 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)。
Multipart 数据
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
。
转发的头信息
当请求经过负载均衡器等代理时,主机、端口和方案可能会发生变化,这使得从客户端的角度创建指向正确主机、端口和方案的链接成为一项挑战。
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](https://example.com/resource)
被发送到一个代理,并且该代理将请求转发到 [localhost:8080/resource](http://localhost:8080/resource)
,那么可以发送一个 X-Forwarded-Host: example.com
头来通知服务器原始主机是 example.com
。
X-Forwarded-Port
虽然 X-Forwarded-Port: <port>
不是一个标准的头部,但它是一个事实上的标准,用于将原始端口信息传递给下游服务器。例如,如果一个请求 [example.com/resource](https://example.com/resource)
被发送到一个代理,然后代理将请求转发到 [localhost:8080/resource](http://localhost:8080/resource)
,那么可以发送一个 X-Forwarded-Port: 443
头部来告知服务器原始端口是 443
。
X-Forwarded-Proto
虽然不是标准,X-Forwarded-Proto: (https|http) 是一个事实上的标准头,用于向下游服务器传达原始协议(例如,https / http)。例如,如果一个请求 [example.com/resource](https://example.com/resource)
被发送到一个代理,该代理将请求转发到 [localhost:8080/resource](http://localhost:8080/resource)
,那么可以发送一个 X-Forwarded-Proto: https
头来通知服务器原始协议是 https
。
X-Forwarded-Ssl
虽然 X-Forwarded-Ssl: (on|off)
不是一个标准,但它是一个事实上的标准头,用于将原始协议(例如,https / http)传递给下游服务器。例如,如果一个请求 [example.com/resource](https://example.com/resource)
被发送到一个代理,该代理将请求转发到 [localhost:8080/resource](http://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 路径中可见,因为应用可能使用不同的子域,这样做的好处包括:
-
增强的安全性,例如,同源策略
-
应用的独立扩展(不同的域指向不同的 IP 地址)
场景 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
代替。
安全注意事项
由于应用程序无法知道转发的头信息是由代理按预期添加的,还是由恶意客户端添加的,因此对于转发的头信息需要考虑安全性。这就是为什么应该配置信任边界的代理以移除来自外部的不受信任的转发流量。你也可以将 ForwardedHeaderTransformer
配置为 removeOnly=true
,在这种情况下,它会移除但不使用这些头信息。
过滤器
在 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
网络过滤器专为此目的而设计。它可以配置为:
-
接收到带有尾部斜杠的 URL 时,响应一个 HTTP 重定向状态,将浏览器发送到没有尾部斜杠的 URL 变体。
-
修改请求,使其表现得像是没有尾部斜杠的请求,并继续处理该请求。
下面是如何为博客应用程序实例化和配置一个 UrlHandlerFilter
:
- Java
- Kotlin
UrlHandlerFilter urlHandlerFilter = UrlHandlerFilter
// will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post"
.trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
// will mutate the request to "/admin/user/account/" and make it as "/admin/user/account"
.trailingSlashHandler("/admin/**").mutateRequest()
.build();
val urlHandlerFilter = UrlHandlerFilter
// will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post"
.trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
// will mutate the request to "/admin/user/account/" and make it as "/admin/user/account"
.trailingSlashHandler("/admin/**").mutateRequest()
.build()
异常
在 WebHandler API 中,你可以使用 WebExceptionHandler
来处理来自 WebFilter
实例链和目标 WebHandler
的异常。在使用 WebFlux 配置 时,注册一个 WebExceptionHandler
就像将其声明为一个 Spring bean 一样简单,并且(可选地)可以通过在 bean 声明上使用 @Order
或实现 Ordered
来表达优先级。
下表描述了可用的 WebExceptionHandler
实现:
异常处理器 | 描述 |
---|---|
ResponseStatusExceptionHandler | 通过将响应设置为异常的 HTTP 状态码,提供对 ResponseStatusException 类型异常的处理。 |
WebFluxResponseStatusExceptionHandler | ResponseStatusExceptionHandler 的扩展,它还可以确定任何异常上的 @ResponseStatus 注解的 HTTP 状态码。此处理器在 WebFlux 配置 中声明。 |
编解码器
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 的异步非阻塞解析器用于将字节块流聚合为
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
,并刷新输出以确保及时传递。
默认情况下,Jackson2Encoder
和 Jackson2Decoder
都不支持 String
类型的元素。相反,默认假设是一个字符串或一系列字符串代表序列化的 JSON 内容,由 CharSequenceEncoder
来渲染。如果你需要的是从 Flux<String>
渲染一个 JSON 数组,可以使用 Flux#collectToList()
并编码一个 Mono<List<String>>
。
表单数据
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()
来重复、类似映射地访问部分,或者依赖 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
属性中(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()
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()