Spring5.1.x官方参考指南中文版

 主页   资讯   文章   代码   电子书 

1.2 Reactive Core

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

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

  • HttpHandler:使用非阻塞I / O和Reactive Streams背压进行HTTP请求处理的基本协定,以及Reactor Netty,Undertow,Tomcat,Jetty以及任何Servlet 3.1+容器的适配器。
  • WebHandler API:稍高级别的通用Web API,用于处理请求,在此之上构建了具体的编程模型,例如带注解的控制器和功能端点。

对于客户端,有一个基本的ClientHttpConnector协定,以执行具有非阻塞I / O和响应流反压力的HTTP请求,以及用于Reactor Netty和响应式Jetty HttpClient的适配器。 应用程序中使用的更高级别的WebClient基于此基本协定。

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

1.2.1 HttpHandler

HttpHandler是具有单个方法的简单协定,用于处理请求和响应。 它故意是最小的,它的主要也是唯一的目的是成为对不同HTTP服务器API的最小抽象。

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

Server name Server API used Reactive Streams support
Netty Netty API Reactor Netty
Undertow Undertow API spring-web: Undertow to Reactive Streams bridge
Tomcat Servlet 3.1 non-blocking I/O; Tomcat API to read and write ByteBuffers vs byte[] spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge
Jetty Servlet 3.1 non-blocking I/O; Jetty API to write ByteBuffers vs byte[] spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge
Servlet 3.1 container Servlet 3.1 non-blocking I/O spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge

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

Server name Group id Artifact name
Reactor Netty io.projectreactor.netty reactor-netty
Undertow io.undertow undertow-core
Tomcat org.apache.tomcat.embed tomcat-embed-core

下面的代码段显示了如何将HttpHandler适配器与每个服务器API一起使用:

Reactor Netty

HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create(host, port).newHandler(adapter).block();

Undertow

HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();

Tomcat

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

Jetty

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

Servlet 3.1+ Container

要将其作为WAR部署到任何Servlet 3.1+容器,您可以扩展WAR并将其包括在AbstractReactiveWebInitializer中。 该类使用ServletHttpHandlerAdapter包装HttpHandler并将其注册为Servlet。

1.2.2 WebHandler API

org.springframework.web.server包基于HttpHandler契约构建,以提供通用的Web API,以通过多个WebExceptionHandler,多个WebFilter和单个WebHandler组件的链来处理请求。 通过简单地指向自动检测组件的Spring ApplicationContext和/或通过向构建器注册组件,可以将该链与WebHttpHandlerBuilder放在一起。

尽管HttpHandler的目标很简单,即抽象化不同HTTP服务器的使用,但WebHandler API的目的是提供Web应用程序中常用的更广泛的功能集,例如:

  • 具有属性的用户会话。
  • 请求属性。
  • 请求的解析的语言环境或主体。
  • 访问已解析和缓存的表单数据。
  • 多部分数据的抽象。

Special bean types

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

Bean name Bean type Count Description
<any> WebExceptionHandler 0..N 提供对WebFilter实例链和目标 链中的异常的处理WebHandler。有关更多详细信息,请参见Exceptions。
<any> WebFilter 0..N 在其余过滤链和目标之前和之后应用拦截式逻辑WebHandler。有关更多详细信息,请参见过滤器。
webHandler WebHandler 1 请求的处理程序。
webSessionManager WebSessionManager 0..1 WebSession通过上的方法公开的实例的管理器ServerWebExchange。 DefaultWebSessionManager默认。
serverCodecConfigurer ServerCodecConfigurer 0..1 用于访问HttpMessageReader实例以解析表单数据和多部分数据,然后通过上的方法公开这些数据ServerWebExchange。ServerCodecConfigurer.create()默认
localeContextResolver LocaleContextResolver 0..1 LocaleContext通过在上的方法公开的解析器ServerWebExchange。 AcceptHeaderLocaleContextResolver默认。
forwardedHeaderTransformer ForwardedHeaderTransformer 0..1 对于处理转发的类型标头,可以通过提取和删除它们或仅通过删除它们来进行。默认不使用。

表格数据

ServerWebExchange 公开以下访问表单数据的方法:

Mono<MultiValueMap<String, String>> getFormData();

DefaultServerWebExchange使用配置的HttpMessageReader将表单数据(application / x-www-form-urlencoded)解析为MultiValueMap。 默认情况下,FormHttpMessageReader配置为由ServerCodecConfigurer Bean使用(请参阅Web Handler API)。

Multipart Data

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

Mono <MultiValueMap <String,Part >> getMultipartData();

DefaultServerWebExchange使用配置的HttpMessageReader <MultiValueMap <String,Part >>将multipart / form-data内容解析为MultiValueMap。 当前,Synchronoss NIO Multipart是唯一受支持的第三方库,并且是我们知道的用于非阻塞解析多部分请求的唯一库。 通过ServerCodecConfigurer bean启用它(请参阅Web Handler API)。

要以流方式解析多部分数据,可以使用从HttpMessageReader <Part>返回的Flux <Part>。 例如,在带注解的控制器中,使用@RequestPart意味着按名称对各个部分进行类似于Map的访问,因此需要完全解析多部分数据。 相反,您可以使用@RequestBody将内容解码为Flux <Part>而不收集到MultiValueMap。

Forwarded Headers

当请求通过代理(例如负载平衡器)进行处理时,主机,端口和方案可能会更改,这使得从客户端角度创建指向正确的主机,端口和方案的链接带来了挑战。

RFC 7239定义了代理可以用来提供有关原始请求的信息的HTTP转发头。还有其他非标准标头,包括X-Forwarded-Host,X-Forwarded-Port,X-Forwarded-Proto,X-Forwarded-Ssl和X-Forwarded-Prefix。

ForwardedHeaderFilter是一个Servlet过滤器,它根据转发的标头修改请求的主机,端口和方案,然后删除这些标头。

对于转发的标头,存在安全方面的考虑,因为应用程序无法知道标头是由代理添加的,还是由恶意客户端添加的。这就是为什么应配置信任边界处的代理以删除来自外部的不受信任的转发标头的原因。您还可以使用removeOnly = true配置ForwardedHeaderFilter,在这种情况下,它将删除但不使用标头。

在5.1版本中,ForwardedHeaderFilter被ForwardedHeaderTransformer弃用并取代,因此可以在创建交换之前更早地处理转发的标头。 如果仍然配置了过滤器,则将其从过滤器列表中删除,而改用ForwardedHeaderTransformer。

1.2.3 Filters

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

CORS

Spring WebFlux通过控制器上的注解为CORS配置提供了细粒度的支持。 但是,当您将其与Spring Security结合使用时,我们建议您依赖内置的CorsFilter,该产品必须在Spring Security的过滤器链之前订购。

有关更多详细信息,请参见有关CORS和CORS WebFilter的部分。

1.2.4 Exceptions

在WebHandler API中,可以使用WebExceptionHandler来处理WebFilter实例链和目标WebHandler链中的异常。 使用WebFlux Config时,注册WebExceptionHandler就像将其声明为Spring bean一样简单,并且(可选)通过在bean声明上使用@Order或实现Ordered来表达优先级。

下表描述了可用的WebExceptionHandler实现:

Exception Handler Description
ResponseStatusExceptionHandler 通过将响应设置为异常的HTTP状态代码,提供对ResponseStatusException类型的异常的处理。
WebFluxResponseStatusExceptionHandler ResponseStatusExceptionHandler的扩展,它也可以确定任何异常时@ResponseStatus批注的HTTP状态代码。该处理程序在WebFlux Config中声明。

1.2.5 Codecs

spring-web和spring-core模块提供支持,通过具有Reactive Streams背压的非阻塞I / O,可以将字节内容与更高级别的对象之间的字节序列进行序列化和反序列化。以下介绍了此支持:

  • 编码器和解码器是底层协议,用于独立于HTTP编码和解码内容。
  • HttpMessageReader和HttpMessageWriter是对HTTP消息内容进行编码和解码的协定。
  • 可以使用EncoderHttpMessageWriter来包装Encoder,以使其适合在Web应用程序中使用,而可以使用DecoderHttpMessageReader来包装Decoder。
  • DataBuffer抽象了不同的字节缓冲区表示形式(例如Netty ByteBuf,java.nio.ByteBuffer等),并且是所有编解码器都在处理的内容。有关此主题的更多信息,请参见“ Spring核心”部分中的数据缓冲区和编解码器。

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的每个块中,每个代表JSON对象。
  • 每个TokenBuffer都传递给Jackson的ObjectMapper以创建更高级别的对象。
  • 解码为单值发布者(例如Mono)时,有一个TokenBuffer。
  • 当解码为多值发布者(例如Flux)时,一旦为一个完整的对象接收到足够的字节,每个TokenBuffer就会传递给ObjectMapper。输入内容可以是JSON数组,如果内容类型为“ application / stream + json”,则可以是行分隔的JSON。

Jackson2Encoder的工作方式如下:

  • 对于单个值发布者(例如Mono),只需通过ObjectMapper对其进行序列化即可。
  • 对于具有“ application / json”的多值发布者,默认情况下使用Flux#collectToList()收集值,然后序列化结果集合。
  • 对于具有流媒体类型(例如application / stream + json或application / stream + x-jackson-smile)的多值发布者,请使用行分隔的JSON格式分别编码,写入和刷新每个值。
  • 对于SSE,将为每个事件调用Jackson2Encoder,并刷新输出以确保交付没有延迟。

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

Form Data

FormHttpMessageReader和FormHttpMessageWriter支持对“ application / x-www-form-urlencoded”内容进行解码和编码。

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

一旦使用getFormData(),就无法再从请求正文中读取原始原始内容。因此,应用程序应始终通过ServerWebExchange来访问缓存的表单数据,而不是从原始请求正文中进行读取。

Multipart

MultipartHttpMessageReader和MultipartHttpMessageWriter支持对“ multipart / form-data”内容进行解码和编码。反过来,MultipartHttpMessageReader委托给另一个HttpMessageReader进行实际解析为Flux <Part>,然后将这些部分简单地收集到MultiValueMap中。目前,Synchronoss NIO Multipart用于实际解析。

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

一旦使用getMultipartData(),就无法再从请求正文中读取原始原始内容。因此,应用程序必须始终使用getMultipartData()来重复,类似地图地访问零件,否则必须依靠SynchronossPartHttpMessageReader来一次性访问Flux <Part>。

Streaming

在流式传输到HTTP响应(例如text / event-stream,application / stream + json)时,定期发送数据很重要,这样才能尽快(而不是稍后)可靠地检测到断开连接的客户端。这样的发送可以是仅注解,空的SSE事件或任何其他可以有效充当心跳的“无操作”数据。

DataBuffer

DataBuffer是WebFlux中字节缓冲区的表示形式。该参考书的Spring Core部分在有关数据缓冲区和编解码器的部分中有更多内容。要理解的关键点是,在诸如Netty之类的某些服务器上,字节缓冲区被池化并且对引用计数进行计数,并且在消耗字节缓冲区时必须将其释放以避免内存泄漏。

WebFlux应用程序通常不需要关心此类问题,除非它们直接使用或产生数据缓冲区,而不是依赖于编解码器与更高级别的对象进行转换。或者,除非他们选择创建自定义编解码器。对于这种情况,请查看“数据缓冲区和编解码器”中的信息,尤其是有关“使用数据缓冲区”的部分。

1.2.6 Logging

Spring WebFlux中的DEBUG级别日志记录被设计为紧凑,最小化和人性化。它侧重于一遍又一遍有用的高价值信息,而其他信息则仅在调试特定问题时才有用。

TRACE级别的日志记录通常遵循与DEBUG相同的原则(例如,也不应成为firehose),但可用于调试任何问题。另外,某些日志消息在TRACE vs DEBUG上可能显示不同级别的详细信息。

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

Log Id

在WebFlux中,单个请求可以在多个线程上执行,并且线程ID对于关联属于特定请求的日志消息没有用。这就是为什么WebFlux日志消息默认情况下带有特定于请求的ID的原因。

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

Sensitive Data

调试和跟踪日志记录可以记录敏感信息。这就是默认情况下屏蔽表单参数和标题的原因,并且必须显式启用它们的完整日志记录。

以下示例显示了如何对服务器端请求执行此操作:

@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().enableLoggingRequestDetails(true);
    }
}

以下示例显示了如何针对客户端请求执行此操作:

Consumer<ClientCodecConfigurer> consumer = configurer ->
        configurer.defaultCodecs().enableLoggingRequestDetails(true);

WebClient webClient = WebClient.builder()
        .exchangeStrategies(ExchangeStrategies.builder().codecs(consumer).build())
        .build();