响应式核心

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

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

    • HttpHandler:具有非阻塞I/O和响应式流背压的HTTP请求处理的基本契约,以及适配器用于 Reactor Netty、Undertow、Tomcat、Jetty 和任何Servlet容器。

    • WebHandler API:略高级别的通用Web API,用于请求处理,其上构建了诸如注解控制器和功能端点等具体编程模型。

  • 对于客户端,有一个基本的ClientHttpConnector契约,用于执行具有非阻塞I/O和响应式流背压的HTTP请求,以及适配器用于 Reactor Netty、响应式 Jetty HttpClientApache HttpComponents。应用程序中使用的更高级别的 WebClient 建立在这个基本契约之上。

  • 对于客户端和服务器,用于HTTP请求和响应内容的序列化和反序列化的编解码器

HttpHandler

HttpHandler是一个简单的接口,只有一个方法用于处理请求和响应。它被设计为最小化,其主要目的是作为不同HTTP服务器API的最小抽象。

以下表格描述了支持的服务器API:

服务器名称 使用的服务器API 响应式流支持

Netty

Netty API

Reactor Netty

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中。该类使用ServletHttpHandlerAdapterHttpHandler包装并将其注册为Servlet

WebHandler API

org.springframework.web.server包基于HttpHandler协议构建,提供了一个通用的Web API,用于通过多个WebExceptionHandler、多个WebFilter和一个单一的WebHandler组件来处理请求的链。可以通过简单地指向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 Handler 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 Handler 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 / 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 路径中可见,应用程序可能使用不同的子域,提供以下好处:

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

  • 应用程序的独立扩展(不同域指向不同 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,这样它会移除但不使用这些头部。

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

ResponseStatusExceptionHandler

Provides handling for exceptions of type ResponseStatusException by setting the response to the HTTP status code of the exception.

WebFluxResponseStatusExceptionHandler

Extension of ResponseStatusExceptionHandler that can also determine the HTTP status code of a @ResponseStatus annotation on any exception.

This handler is declared in the WebFlux Config.

编解码器

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的异步、非阻塞解析器将字节块流聚合为每个代表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-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>

限制

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

DecoderHttpMessageReader是否公开了 maxInMemorySize属性,如果是,Javadoc将详细介绍默认值。在服务器端, ServerCodecConfigurer提供了一个设置所有编解码器的单一位置,参见 HTTP消息编解码器。在客户端端,可以在 WebClient.Builder中更改所有编解码器的限制。

多部分解析maxInMemorySize属性限制非文件部分的大小。对于写入磁盘的文件部分,它确定部分写入磁盘的阈值。对于写入磁盘的文件部分,还有一个 maxDiskUsagePerPart属性,用于限制每个部分的磁盘空间量。还有一个 maxParts属性,用于限制多部分请求中的总部分数。要在WebFlux中配置所有这三个属性,您需要向 ServerCodecConfigurer提供一个预配置的 MultipartHttpMessageReader实例。

流式

text/event-streamapplication/x-ndjson),定期发送数据非常重要,以便更早地可靠地检测到断开连接的客户端。这样的发送可以是仅注释、空SSE事件或任何其他有效作为心跳的“无操作”数据。

数据缓冲区

数据缓冲区是WebFlux中字节缓冲区的表示。关于这一点,Spring Core部分的参考资料在数据缓冲区和编解码器部分有更多介绍。需要理解的关键点是,在一些服务器(如Netty)上,字节缓冲区是被池化和引用计数的,当消耗时必须释放,以避免内存泄漏。

WebFlux应用程序通常不需要关注这些问题,除非它们直接消耗或产生数据缓冲区,而不是依赖编解码器将其转换为更高级别的对象,或者除非它们选择创建自定义编解码器。对于这种情况,请查看数据缓冲区和编解码器中的信息,特别是关于使用数据缓冲区的部分。

日志记录

DEBUG级别的Spring WebFlux日志记录旨在紧凑、最小化且用户友好。它侧重于高价值的信息位,这些信息一遍又一遍地有用,而其他信息只在调试特定问题时有用。

TRACE级别的日志记录通常遵循与DEBUG相同的原则(例如,也不应该是一个firehose),但可用于调试任何问题。此外,一些日志消息在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()