响应式核心

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 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 的桥接

下表描述了服务器依赖项(另请参阅 支持的版本

服务器名称 组 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 容器,您可以在 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 应用程序中常用的更广泛的功能集,例如

  • 具有属性的用户会话。

  • 请求属性。

  • 为请求解析的LocalePrincipal

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

  • 多部分数据的抽象。

  • 等等..

特殊 Bean 类型

下表列出了WebHttpHandlerBuilder 可以自动检测 Spring ApplicationContext 中的组件,或者可以直接向其注册的组件

Bean 名称 Bean 类型 计数 描述

<任何>

WebExceptionHandler

0..N

WebFilter 实例链和目标WebHandler 中的异常提供处理。有关更多详细信息,请参阅 异常

<任何>

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 处理程序 API)。

多部分数据

ServerWebExchange 公开以下方法以访问多部分数据

  • 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 处理程序 API)。

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

转发标头

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

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/https)传达给下游服务器。例如,如果将 example.com/resource 的请求发送到将请求转发到 localhost:8080/resource 的代理,则可以发送 X-Forwarded-Proto: https 标头来通知服务器原始协议是 https

X-Forwarded-Ssl

虽然不是标准,但 X-Forwarded-Ssl: (on|off) 是一个事实上的标准标头,用于将原始协议(例如,https/https)传达给下游服务器。例如,如果将 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} -> https://127.0.0.1:8080/app1/{path}

前缀是捕获组 {path} 之前的路径开头。对于代理,前缀是 /api,而对于服务器,前缀是 /app1。在这种情况下,代理可以发送 X-Forwarded-Prefix: /api 以使原始前缀 /api 覆盖服务器前缀 /app1

场景 2:删除路径前缀

有时,应用程序可能希望删除前缀。例如,考虑以下代理到服务器的映射

https://app1.example.com/{path} -> https://127.0.0.1:8080/app1/{path}
https://app2.example.com/{path} -> https://127.0.0.1:8080/app2/{path}

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

此部署场景的一个常见案例是,许可证按生产应用程序服务器付费,并且最好每个服务器部署多个应用程序以降低费用。另一个原因是在同一服务器上运行更多应用程序以共享服务器运行所需的资源。

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

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

  • 应用程序的独立扩展(不同的域指向不同的 IP 地址)

场景 3:插入路径前缀

在其他情况下,可能需要在前面添加前缀。例如,考虑以下代理到服务器的映射

https://example.com/api/app1/{path} -> https://127.0.0.1:8080/app1/{path}

在这种情况下,代理具有 /api/app1 的前缀,而服务器具有 /app1 的前缀。代理可以发送 X-Forwarded-Prefix: /api/app1 以使原始前缀 /api/app1 覆盖服务器前缀 /app1

ForwardedHeaderTransformer

ForwardedHeaderTransformer 是一个组件,它根据转发标头修改请求的主机、端口和方案,然后删除这些标头。如果您将其声明为名为 forwardedHeaderTransformer 的 bean,它将被 检测到 并使用。

在 5.1 中,ForwardedHeaderFilter 已弃用,并由 ForwardedHeaderTransformer 取代,以便可以在创建交换之前更早地处理转发标头。如果过滤器仍然配置,它将从过滤器列表中删除,并改为使用 ForwardedHeaderTransformer

安全注意事项

转发标头存在安全考虑因素,因为应用程序无法知道标头是否按预期由代理添加,还是由恶意客户端添加。这就是为什么应该配置信任边界处的代理以删除来自外部的不可信转发流量的原因。您还可以使用 removeOnly=true 配置 ForwardedHeaderTransformer,在这种情况下,它会删除标头,但不会使用它们。

过滤器

WebHandler API 中,您可以使用 WebFilter 在过滤器处理链的其余部分和目标 WebHandler 之前和之后应用拦截样式逻辑。当使用 WebFlux 配置 时,注册 WebFilter 就像将其声明为 Spring bean 并(可选地)通过在 bean 声明上使用 @Order 或通过实现 Ordered 来表示优先级一样简单。

CORS

Spring WebFlux 通过控制器上的注释提供了对 CORS 配置的细粒度支持。但是,当您将其与 Spring Security 一起使用时,我们建议依赖内置的 CorsFilter,该过滤器必须在 Spring Security 的过滤器链之前排序。

有关更多详细信息,请参阅有关 CORSCORS WebFilter 的部分。

URL 处理程序

您可能希望您的控制器端点匹配 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 等编码器和解码器,以及表单数据、多部分内容、服务器发送事件等 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 文本序列。

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 访问缓存的表单数据,而不是从原始请求正文读取。

多部分

MultipartHttpMessageReaderMultipartHttpMessageWriter 支持解码和编码“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>

协议缓冲区

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 变体支持在 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 中更改。

对于 多部分解析maxInMemorySize 属性限制非文件部分的大小。对于文件部分,它确定将部分写入磁盘的阈值。对于写入磁盘的文件部分,还有一个 maxDiskUsagePerPart 属性来限制每个部分的磁盘空间量。还有一个 maxParts 属性来限制多部分请求中部件的总数。要在 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()

追加器

诸如 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()