响应式核心

spring-web 模块包含以下对响应式 Web 应用程序的基础支持

  • 对于服务器请求处理,有两种级别的支持。

    • HttpHandler:使用非阻塞 I/O 和 Reactive Streams 反压进行 HTTP 请求处理的基本契约,以及 Reactor Netty、Undertow、Tomcat、Jetty 和任何 Servlet 容器的适配器。

    • WebHandler API:更高一级、通用的 Web API,用于处理请求,在其之上构建了具体的编程模型,例如注解控制器和函数式端点。

  • 对于客户端,有一个基本的 ClientHttpConnector 契约,用于使用非阻塞 I/O 和 Reactive Streams 反压执行 HTTP 请求,以及 Reactor Netty、响应式 Jetty HttpClientApache 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 而非 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 名称

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 容器,您可以在 WAR 中扩展并包含 AbstractReactiveWebInitializer。该类将 HttpHandler 封装在 ServletHttpHandlerAdapter 中,并将其注册为 Servlet

WebHandler API

org.springframework.web.server 包构建在HttpHandler 契约之上,提供了一个通用的 Web API,用于通过多个 WebExceptionHandler、多个 WebFilter 和一个 WebHandler 组件组成的链来处理请求。可以通过 WebHttpHandlerBuilder 将此链组合起来,只需指向一个 Spring ApplicationContext(组件在此处自动检测),和/或向构建器注册组件。

尽管 HttpHandler 的目标是抽象不同 HTTP 服务器的使用,但 WebHandler API 旨在提供 Web 应用程序中常用的一组更广泛的功能,例如

  • 带有属性的用户会话。

  • 请求属性。

  • 请求的解析后的 LocalePrincipal

  • 访问已解析和缓存的表单数据。

  • Multipart 数据抽象。

  • 等等..

特殊 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 实例,以解析表单数据和 multipart 数据,这些数据随后通过 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。默认情况下,ServerCodecConfigurer bean 配置了 FormHttpMessageReader(请参阅Web Handler API)。

Multipart 数据

ServerWebExchange 暴露以下方法用于访问 multipart 数据

  • Java

  • Kotlin

Mono<MultiValueMap<String, Part>> getMultipartData();
suspend fun getMultipartData(): MultiValueMap<String, Part>

DefaultServerWebExchange 使用配置的 HttpMessageReader<MultiValueMap<String, Part>>multipart/form-datamultipart/mixedmultipart/related 内容解析为 MultiValueMap。默认情况下,这是 DefaultPartHttpMessageReader,它没有任何第三方依赖。或者,可以使用基于 Synchronoss NIO Multipart 库的 SynchronossPartHttpMessageReader。两者都通过 ServerCodecConfigurer bean 进行配置(请参阅Web Handler API)。

要以流式方式解析 multipart 数据,可以使用 PartEventHttpMessageReader 返回的 Flux<PartEvent>,而不是使用 @RequestPart,因为 @RequestPart 意味着按名称对单个部分进行类似 Map 的访问,因此需要完整解析 multipart 数据。相比之下,可以使用 @RequestBody 将内容解码为 Flux<PartEvent>,而无需收集到 MultiValueMap

Forwarded 头部

当请求经过负载均衡器等代理时,主机、端口和方案可能会发生变化,这使得从客户端角度创建指向正确主机、端口和方案的链接成为一项挑战。

RFC 7239 定义了 Forwarded HTTP 头部,代理可以使用它来提供关于原始请求的信息。

非标准头部

还有其他非标准头部,包括 X-Forwarded-HostX-Forwarded-PortX-Forwarded-ProtoX-Forwarded-SslX-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}

代理没有前缀,而应用程序 app1app2 分别具有路径前缀 /app1/app2。代理可以发送 X-Forwarded-Prefix: ,以便空前缀覆盖服务器前缀 /app1/app2

这种部署场景的一个常见情况是,许可证按生产应用服务器付费,因此更倾向于每台服务器部署多个应用以降低费用。另一个原因是同一台服务器上运行更多应用,以共享服务器运行所需的资源。

在这些场景中,应用程序需要非空的上下文根,因为同一台服务器上有多个应用程序。但是,这不应在公共 API 的 URL 路径中可见,公共 API 中应用程序可以使用不同的子域,这提供了诸如以下好处

  • 增加安全性,例如同源策略

  • 应用程序的独立扩缩容(不同域指向不同 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 Handler

您可能希望控制器端点匹配 URL 路径中带或不带尾部斜杠的路由。例如,“GET /home”和“GET /home/”都应该由使用 @GetMapping("/home") 注解的控制器方法处理。

将尾部斜杠变体添加到所有映射声明不是处理此用例的最佳方法。UrlHandlerFilter Web 过滤器就是为此目的设计的。它可以配置为

  • 当接收到带有尾部斜杠的 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-webspring-core 模块通过 Reactive Streams 反压机制的非阻塞 I/O,提供对字节内容与更高级别对象之间的序列化和反序列化支持。以下描述了此支持

  • EncoderDecoder 是与 HTTP 无关的内容编码和解码的低级别契约。

  • HttpMessageReaderHttpMessageWriter 是编码和解码 HTTP 消息内容的契约。

  • 一个 Encoder 可以用 EncoderHttpMessageWriter 进行包装以适应 Web 应用中的使用,而一个 Decoder 可以用 DecoderHttpMessageReader 进行包装。

  • DataBuffer 抽象了不同的字节缓冲区表示(例如,Netty ByteBufjava.nio.ByteBuffer 等),并且是所有编解码器工作的基础。有关此主题的更多信息,请参阅“Spring Core”部分中的 数据缓冲区和编解码器

spring-core 模块提供了 byte[]ByteBufferDataBufferResourceString 编码器和解码器实现。spring-web 模块提供了 Jackson JSON、Jackson Smile、JAXB2、Protocol Buffers 以及其他编码器和解码器,以及用于表单数据、multipart 内容、服务器发送事件等的仅用于 Web 的 HTTP 消息读取器和写入器实现。

ClientCodecConfigurerServerCodecConfigurer 通常用于配置和自定义应用中使用的编解码器。请参阅配置 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-ndjsonapplication/stream+x-jackson-smile)的多值发布者,使用 行分隔 JSON 格式对每个值进行单独编码、写入和刷新。可以向编码器注册其他流式媒体类型。

  • 对于 SSE,每发生一个事件就会调用 Jackson2Encoder,并且会刷新输出以确保及时发送。

默认情况下,Jackson2EncoderJackson2Decoder 都不支持 String 类型元素。相反,默认的假设是字符串或字符串序列表示序列化的 JSON 内容,由 CharSequenceEncoder 渲染。如果需要从 Flux<String> 渲染 JSON 数组,请使用 Flux#collectToList() 并编码一个 Mono<List<String>>

表单数据

FormHttpMessageReaderFormHttpMessageWriter 支持解码和编码 application/x-www-form-urlencoded 内容。

在服务器端,表单内容通常需要从多个位置访问,ServerWebExchange 提供了一个专用的 getFormData() 方法,该方法通过 FormHttpMessageReader 解析内容,然后缓存结果以便重复访问。请参阅 WebHandler API 部分中的 表单数据

一旦使用了 getFormData(),原始的原始内容就不能再从请求体中读取了。因此,应用程序应该始终通过 ServerWebExchange 访问缓存的表单数据,而不是直接从原始请求体中读取。

Multipart

MultipartHttpMessageReaderMultipartHttpMessageWriter 支持解码和编码 "multipart/form-data"、"multipart/mixed" 和 "multipart/related" 内容。进而,MultipartHttpMessageReader 将实际解析为 Flux<Part> 的工作委托给另一个 HttpMessageReader,然后简单地将各部分收集到一个 MultiValueMap 中。默认情况下使用 DefaultPartHttpMessageReader,但这可以通过 ServerCodecConfigurer 进行更改。有关 DefaultPartHttpMessageReader 的更多信息,请参阅 DefaultPartHttpMessageReader 的 javadoc

在服务器端,multipart 表单内容可能需要从多个位置访问,ServerWebExchange 提供了一个专用的 getMultipartData() 方法,该方法通过 MultipartHttpMessageReader 解析内容,然后缓存结果以便重复访问。请参阅 WebHandler API 部分中的 Multipart 数据

一旦使用了 getMultipartData(),原始的原始内容就不能再从请求体中读取了。因此,应用程序必须始终使用 getMultipartData() 进行对各部分的重复的、类似 map 的访问,或者依赖 SynchronossPartHttpMessageReader 进行对 Flux<Part> 的一次性访问。

Protocol Buffers

ProtobufEncoderProtobufDecoder 支持解码和编码用于 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 及更高版本。

ProtobufJsonDecoderProtobufJsonEncoder 变体支持将 JSON 文档读取到 Protobuf 消息以及将 Protobuf 消息写入 JSON 文档。它们需要 "com.google.protobuf:protobuf-java-util" 依赖。请注意,JSON 变体不支持读取消息流,更多详细信息请参阅 ProtobufJsonDecoder 的 javadoc

限制

对输入流进行部分或全部缓冲的 DecoderHttpMessageReader 实现可以配置内存中最大缓冲字节数的限制。在某些情况下,缓冲是因为输入被聚合并表示为单个对象——例如,使用 @RequestBody byte[]x-www-form-urlencoded 数据等的控制器方法。缓冲也可以发生在流式传输中,当拆分输入流时——例如,分隔文本、JSON 对象流等。对于这些流式传输情况,限制适用于流中与一个对象关联的字节数。

要配置缓冲区大小,您可以检查给定的 DecoderHttpMessageReader 是否暴露了 maxInMemorySize 属性,如果暴露了,Javadoc 中将包含默认值的详细信息。在服务器端,ServerCodecConfigurer 提供了一个设置所有编解码器的统一入口,请参阅 HTTP 消息编解码器。在客户端,可以在 WebClient.Builder 中更改所有编解码器的限制。

对于 Multipart 解析maxInMemorySize 属性限制非文件部分的尺寸。对于文件部分,它决定了将该部分写入磁盘的阈值。对于写入磁盘的文件部分,还有一个额外的 maxDiskUsagePerPart 属性,用于限制每个部分的磁盘空间量。还有一个 maxParts 属性,用于限制 multipart 请求中的总部件数量。要在 WebFlux 中配置这三个属性,您需要向 ServerCodecConfigurer 提供预先配置的 MultipartHttpMessageReader 实例。

流式传输

当向 HTTP 响应进行流式传输(例如,text/event-streamapplication/x-ndjson)时,定期发送数据非常重要,以便更及时可靠地检测到断开连接的客户端。这样的发送可以是仅包含注释的空 SSE 事件,或任何其他能够有效充当心跳的“无操作”数据。

DataBuffer

DataBuffer 是 WebFlux 中字节缓冲区的表示。本参考文档的 Spring Core 部分在 数据缓冲区和编解码器 一节中有更多介绍。关键要理解的是,在 Netty 等一些服务器上,字节缓冲区是池化和引用计数的,使用后必须释放以避免内存泄漏。

WebFlux 应用程序通常无需关注此类问题,除非它们直接使用或产生数据缓冲区,而不是依赖编解码器在更高级别的对象之间进行转换,或者除非它们选择创建自定义编解码器。对于此类情况,请查看 数据缓冲区和编解码器 中的信息,尤其是 使用 DataBuffer 一节。

日志

Spring WebFlux 中的 DEBUG 级别日志被设计为紧凑、最小化且对人友好。它侧重于反复有用的高价值信息片段,而不是仅在调试特定问题时有用的其他信息。

TRACE 级别日志通常遵循与 DEBUG 相同的原则(例如也不应是信息洪流),但可用于调试任何问题。此外,某些日志消息在 TRACEDEBUG 级别可能显示不同程度的详细信息。

良好的日志记录来自于使用日志的经验。如果您发现任何不符合既定目标的地方,请告知我们。

日志 ID

在 WebFlux 中,单个请求可以在多个线程上运行,而线程 ID 对于关联属于特定请求的日志消息没有用。因此,WebFlux 日志消息默认会带有请求特定的 ID 前缀。

在服务器端,日志 ID 存储在 ServerWebExchange 属性 (LOG_ID_ATTRIBUTE) 中,而基于该 ID 的完全格式化前缀可通过 ServerWebExchange#getLogPrefix() 获取。在 WebClient 端,日志 ID 存储在 ClientRequest 属性 (LOG_ID_ATTRIBUTE) 中,而完全格式化前缀可通过 ClientRequest#logPrefix() 获取。

敏感数据

DEBUGTRACE 级别的日志可能会记录敏感信息。因此,表单参数和请求头默认被屏蔽,您必须显式启用它们的完整日志记录。

以下示例展示了如何对服务器端请求进行此操作

  • 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()