反应式核心(Reactive Core)
spring-web 模块包含了以下针对反应式 Web 应用程序的基础支持:
-
在服务器请求处理方面,提供了两个层次的支持。
-
HttpHandler:用于HTTP请求处理的基本接口,支持非阻塞I/O和Reactive Streams的背压机制,并提供了针对Reactor Netty、Tomcat、Jetty以及任何Servlet容器的适配器。
-
WebHandler API:比HttpHandler层次稍高一些的通用Web API,用于请求处理;在此基础上可以构建诸如带注解的控制器(annotated controllers)和函数式端点(functional endpoints)等具体的编程模型。
-
-
在客户端方面,有一个基本的
ClientHttpConnector接口,支持非阻塞I/O和Reactive Streams的背压机制,并提供了针对Reactor Netty、Jetty HttpClient以及Apache HttpComponents的适配器。应用程序中使用的更高级别的WebClient就是基于这个基本接口构建的。 -
对于客户端和服务器,还有codecs用于HTTP请求和响应内容的序列化与反序列化。
HttpHandler
HttpHandler 是一个简单的接口,它包含一个方法用于处理请求和响应。该接口被有意设计得非常简洁,其主要且唯一的目的就是作为对不同HTTP服务器API的一种最小化抽象。
下表描述了所支持的服务器API:
| 服务器名称 | 使用的服务器API | 对Reactive Streams的支持 |
|---|---|---|
| Netty | Netty API | Reactor Netty |
| 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 |
| 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()
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 = ...
JettyCoreHttpHandlerAdapter adapter = new JettyCoreHttpHandlerAdapter(handler);
Server server = new Server();
server.setHandler(adapter);
ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();
val handler: HttpHandler = ...
val adapter = JettyCoreHttpHandlerAdapter(handler)
val server = Server()
server.setHandler(adapter)
val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()
在Spring Framework 6.2中,JettyHttpHandlerAdapter已被弃用,取而代之的是JettyCoreHttpHandlerAdapter,后者可以直接与Jetty 12的API集成,无需通过Servlet层。
如果想将其作为WAR文件部署到Servlet容器中,可以使用AbstractReactiveWebInitializer,通过ServletHttpHandlerAdapter将HttpHandler适配为Servlet。
WebHandler API
org.springframework.web.server 包基于 HttpHandler 接口进行构建,旨在提供一个通用的 Web API,用于通过多个 WebExceptionHandler、多个 WebFilter 以及单个 WebHandler 组件来处理请求。可以通过简单指向 Spring 的 ApplicationContext(其中组件能够被 自动检测)来使用 WebHttpHandlerBuilder 搭建这个处理链,或者也可以通过向构建器注册组件来实现。
虽然HttpHandler的目标是简化对不同HTTP服务器的使用,但WebHandler API旨在提供一系列在Web应用程序中常用的更广泛的功能,例如:
- 带有属性的用户会话。
- 请求属性。
- 为请求解析的
Locale或Principal。 - 访问已解析和缓存的形式数据。
- 多部分数据的抽象处理。
- 以及更多功能。
特殊bean类型
下表列出了WebHttpHandlerBuilder能够在Spring ApplicationContext中自动检测到的组件,或者可以直接与之注册的组件:
| Bean 名称 | Bean 类型 | 数量 | 描述 |
|---|---|---|---|
| <any> | WebExceptionHandler | 0..N | 用于处理来自 WebFilter 实例链及目标 WebHandler 的异常。详情请参见 Exceptions。 |
| <any> | WebFilter | 0..N | 在过滤器链的其余部分及目标 WebHandler 之前和之后应用拦截逻辑。详情请参见 Filters。 |
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)。
多部分数据
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中。
转发头部
当一个请求通过负载均衡器等代理服务器时,主机、端口和协议(scheme)可能会发生变化,这使得从客户端的角度出发创建指向正确主机、端口和协议的链接变得具有挑战性。
RFC 7239 定义了 Forwarded HTTP 头部,代理服务器可以使用该头部来提供关于原始请求的信息。
非标准头部
还有其他非标准的头部信息,包括X-Forwarded-Host、X-Forwarded-Port、X-Forwarded-Proto、X-Forwarded-Ssl、X-Forwarded-Prefix和X-Forwarded-For。
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,使其变为空前缀。
在这种部署场景中,一个常见的情况是按照每台生产应用服务器来支付许可证费用,为了降低费用,通常会在每台服务器上部署多个应用程序。另一个原因是希望在同一台服务器上运行更多的应用程序,以便共享该服务器所需的资源。
在这些场景下,由于同一台服务器上运行有多个应用程序,因此需要一个非空的上下文根(context root)。然而,这一点不应在公共API的URL路径中体现出来,因为各应用程序可以使用不同的子域名,这样能带来以下好处:
- 增强安全性,例如,遵循同源策略(same origin policy);
- 实现应用程序的独立扩展(不同的域名对应不同的IP地址)。
场景3:插入路径前缀
在其他情况下,可能有必要在前面加上一个前缀。例如,考虑以下代理到服务器的映射:
https://example.com/api/app1/{path} -> http://localhost:8080/app1/{path}
在这种情况下,代理的前缀为 /api/app1,服务器的前缀也为 /app1。代理可以发送 X-Forwarded-Prefix: /api/app1,以便让原始前缀 /api/app1 覆盖服务器的前缀 /app1。
X-Forwarded-For
X-Forwarded-For: <address> 是一个事实上的标准头部字段,用于将客户端最初的 InetSocketAddress 信息传递给下游服务器。例如,如果一个请求是由地址为 [fd00:fefe:1::4] 的客户端发送到地址为 192.168.0.1 的代理服务器的,那么HTTP请求中包含的“远程地址”信息将反映的是客户端的实际地址,而不是代理服务器的地址。
ForwardedHeaderTransformer
ForwardedHeaderTransformer是一个组件,它根据转发的头部信息(forwarded headers)修改请求的主机、端口和协议(scheme),然后移除这些头部信息。如果你将其声明为一个名为forwardedHeaderTransformer的Bean,它将会被检测到并被使用。
安全考虑
对于转发的头部信息,存在安全方面的考虑:应用程序无法确定这些头部信息是如预期那样由代理添加的,还是由恶意客户端添加的。因此,在信任边界处的代理应被配置为移除来自外部的不可信转发流量。您还可以将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 Config时,注册WebExceptionHandler非常简单,只需将其声明为Spring bean,并且(可选地)通过在bean声明上使用@Order注解或实现Ordered接口来指定优先级即可。
下表描述了可用的 WebExceptionHandler 实现:
| 异常处理器 | 描述 |
|---|---|
ResponseStatusExceptionHandler | 通过将响应设置为异常的HTTP状态码,来处理ResponseStatusException类型的异常。 |
WebFluxResponseStatusExceptionHandler | ResponseStatusExceptionHandler的扩展版本,还可以确定任何带有@ResponseStatus注解的异常的HTTP状态码。此处理器在WebFlux配置中声明。 |
编解码器
spring-web和spring-core模块通过非阻塞I/O和Reactive Streams的背压机制,提供了将字节内容序列化为更高级对象以及从更高级对象反序列化为字节内容的支持。以下是对此支持的描述:
-
HttpMessageReader 和 [HttpMessageWriter](https://docs.spring.io/spring-framework/docs/7.0.3/javadoc-api/org/springframework(http/codec/HttpMessageWriter.html) 是用于编码和解码 HTTP 消息内容的接口。
-
可以将
Encoder与EncoderHttpMessageWriter结合使用,以便在 Web 应用程序中使用;同样,也可以将Decoder与DecoderHttpMessageReader结合使用。 -
DataBuffer 抽象了不同的字节缓冲区表示形式(例如,Netty 的
ByteBuf、java.nio.ByteBuffer等),所有编解码器都是基于此进行操作的。有关此主题的更多信息,请参阅“Spring Core”部分中的 Data Buffers and Codecs。
spring-core 模块提供了 byte[]、ByteBuffer、DataBuffer、Resource 和 String 的编码器及解码器实现。spring-web 模块提供了 Jackson JSON、Jackson Smile、JAXB2、Protocol Buffers 等编码器和解码器,同时还提供了专门用于 Web 的 HTTP 消息读写器实现,这些读写器可以处理表单数据、多部分内容(multipart content)、服务器发送的事件(server-sent events)等。
ClientCodecConfigurer 和 ServerCodecConfigurer 通常用于配置和自定义应用程序中使用的编码解码器(codec)。请参阅关于配置HTTP消息编码解码器的章节。
Jackson JSON
当Jackson库存在时,JSON和二进制JSON(Smile)都受到支持。
JacksonJsonDecoder 的工作原理如下:
-
Jackson的异步、非阻塞解析器用于将字节块流聚合到
TokenBuffer中,每个TokenBuffer代表一个JSON对象。 -
每个
TokenBuffer都被传递给Jackson的JsonMapper以创建更高级别的对象。 -
当解码为单值发布者(例如
Mono)时,只有一个TokenBuffer。 -
当解码为多值发布者(例如
Flux)时,一旦接收到足够多的字节来形成一个完整的对象,每个TokenBuffer就会立即被传递给JsonMapper。输入内容可以是一个JSON数组,或者是任何行分隔的JSON格式,如NDJSON、JSON Lines或JSON Text Sequences。
JacksonJsonEncoder 的工作原理如下:
- 对于单个值发布者(例如
Mono),只需通过JsonMapper进行序列化即可。 - 对于使用
application/json格式的多值发布者,默认情况下使用Flux#collectToList()收集这些值,然后对收集到的集合进行序列化。 - 对于使用流媒体类型(如
application/x-ndjson或application/stream+x-jackson-smile)的多值发布者,需要使用 行分隔 JSON 格式逐个编码、写入并刷新每个值。其他流媒体类型也可以通过编码器进行处理。 - 对于 SSE(Server-Sent Events),每次事件都会调用
JacksonJsonEncoder,并且输出会被立即刷新以确保无延迟地传递。
默认情况下,JacksonJsonEncoder 和 JacksonJsonDecoder 都不支持类型为 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来访问缓存的表单数据,而不是直接从原始请求体中读取数据。
多部分数据
MultipartHttpMessageReader 和 MultipartHttpMessageWriter 支持解码和编码 “multipart/form-data”、“multipart/mixed” 以及 “multipart/related” 类型的内容。MultipartHttpMessageReader 会委托给另一个 HttpMessageReader 来实际解析这些内容,并将解析结果收集到 MultiValueMap 中。默认情况下,使用的是 DefaultPartHttpMessageReader,但可以通过 ServerCodecConfigurer 来更改这一设置。有关 DefaultPartHttpMessageReader 的更多信息,请参考 DefaultPartHttpMessageReader 的 Javadoc。
在服务器端,当需要从多个位置访问多部分表单内容时,ServerWebExchange 提供了一个专门的 getMultipartData() 方法,该方法通过 MultipartHttpMessageReader 来解析内容,然后将结果缓存起来以便后续重复访问。详情请参阅 WebHandler API 中的 Multipart Data 部分。
一旦使用了getMultipartData(),就无法再从请求体中读取原始的原始内容了。因此,应用程序必须始终使用getMultipartData()来反复、类似映射的方式访问各个部分,或者否则就依赖于SynchronossPartHttpMessageReader来一次性获取Flux<Part>。
Protocol Buffers
ProtobufEncoder 和 ProtobufDecoder 支持对 com.google.protobuf.Message 类型进行“application/x-protobuf”、“application/octet-stream”以及“application/vndgoogle.protobuf”格式的内容的解码和编码。如果接收/发送的内容类型中包含了“delimited”参数(例如“application/x-protobuf;delimited=true”),它们还支持值的流式处理。这需要使用版本为 3.29 及以上的 “com.google.protobuf:protobuf-java” 库。
ProtobufJsonDecoder 和 ProtobufJsonEncoder 变体支持将 JSON 文档读写为 Protobuf 消息,以及将 Protobuf 消息读写为 JSON 文档。它们需要依赖 “com.google.protobuf:protobuf-java-util”。注意,这些 JSON 变体不支持读取消息流,有关更多详细信息,请参阅 ProtobufJsonDecoder 的 Javadoc。
Google Gson
应用程序可以利用Gson库中的GsonEncoder和GsonDecoder来序列化和反序列化JSON文档。该编解码器支持JSON媒体类型以及用于流传输的NDJSON格式。
Gson 不支持非阻塞解析,因此 GsonDecoder 也不支持将数据反序列化为 Flux<*> 类型。例如,如果使用该解码器来反序列化一个 JSON 流或甚至是一个作为 Flux<*> 的元素列表,运行时将会抛出 UnsupportedOperationException。应用程序应专注于反序列化有界集合,并使用 Mono<List<*>> 作为目标类型。
限制
Decoder和HttpMessageReader的实现可以对内存中缓冲的最大字节数进行配置。在某些情况下,之所以需要缓冲,是因为输入数据被聚合并表示为单个对象——例如,带有@RequestBody byte[]的控制器方法、x-www-form-urlencoded格式的数据等。在流处理场景中,当需要对输入流进行分割时(例如,分隔文本、JSON对象流等),也会发生缓冲。对于这些流处理情况,该限制适用于流中每个对象所关联的字节数。
要配置缓冲区大小,可以检查给定的 Decoder 或 HttpMessageReader 是否提供了 maxInMemorySize 属性,如果提供的话,Javadoc 中会有关于默认值的详细信息。在服务器端,ServerCodecConfigurer 提供了一个统一的位置来设置所有编解码器,详情请参见 HTTP 消息编解码器。在客户端,可以通过 WebClient.Builder 来更改所有编解码器的限制。
对于Multipart解析,maxInMemorySize属性限制了非文件部分的大小。对于文件部分,它决定了该部分被写入磁盘的阈值。对于被写入磁盘的文件部分,还有一个额外的maxDiskUsagePerPart属性来限制每个部分占用的磁盘空间量。还有一个maxParts属性来限制多部分请求中的总部分数量。要在WebFlux中配置这三个属性,你需要向ServerCodecConfigurer提供一个预先配置好的MultipartHttpMessageReader实例。
流式传输
在向HTTP响应流传输数据时(例如,text/event-stream、application/x-ndjson),定期发送数据非常重要,这样可以更早地可靠地检测到断开的客户端。这种发送可以是仅包含注释的空SSE事件,或者是任何其他“无操作”数据,这些数据实际上可以起到心跳信号的作用。
DataBuffer
DataBuffer是WebFlux中字节缓冲区的表示形式。关于这一点的更多信息,可以在Spring Core部分的数据缓冲区和编解码器章节中找到。需要理解的关键点是,在Netty这样的某些服务器上,字节缓冲区会被复用(即进行池化管理)并带有引用计数机制,因此在使用完毕后必须释放这些缓冲区,以避免内存泄漏。
WebFlux应用程序通常不需要担心这类问题,除非它们直接消耗或生成数据缓冲区(data buffers),而不是依赖于编解码器(codecs)来将数据转换为更高级别的对象,或者除非它们选择创建自定义的编解码器。对于这种情况,请参阅数据缓冲区和编解码器中的信息,特别是使用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
class MyConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true);
}
}
@Configuration
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()