spring-framework / 5.1.3.RELEASE / reference / web-reactive.html

网上反应堆

文档的此部分涵盖对基于Reactive Streams API 构建的反应堆式 Web 应用程序的支持,该应用程序可在非阻塞服务器(例如 Netty,Undertow 和 Servlet 3.1 容器)上运行。各章介绍Spring WebFlux框架,响应式WebClient,对testing的支持和reactive libraries。对于 Servlet 堆栈 Web 应用程序,请参见Web on Servlet 堆栈

1. Spring WebFlux

Spring 框架中包含的原始 Web 框架 Spring Web MVC 是专门为 Servlet API 和 Servlet 容器构建的。Reactive 堆栈 Web 框架 Spring WebFlux 在更高版本 5.0 中添加。它是完全非阻塞的,支持Reactive Streams背压,并在 Netty,Undertow 和 Servlet 3.1 容器等服务器上运行。

这两个 Web 框架都镜像其源模块的名称(spring-webmvcspring-webflux),并在 Spring 框架中并排共存。每个模块都是可选的。应用程序可以使用一个模块或另一个模块,或者在某些情况下同时使用两个模块,例如,带有响应WebClient的 Spring MVC 控制器。

1.1. Overview

为什么创建 Spring WebFlux?

答案的一部分是需要一个非阻塞式的 Web 堆栈来处理少量线程的并发并使用更少的硬件资源进行扩展。 Servlet 3.1 确实提供了用于非阻塞 I/O 的 API。但是,使用它会导致 Servlet API 的其余部分偏离,在 Servlet API 中,Contract 是同步的(FilterServlet)或阻塞的(getParametergetPart)。这是促使新的通用 API 成为所有非阻塞运行时的基础的动机。这很重要,因为在异步,非阻塞空间中构建良好的服务器(例如 Netty)。

答案的另一部分是函数式编程。就像在 Java 5 中添加 Comments 会 Creating 机会(例如带 Comments 的 REST 控制器或单元测试)一样,在 Java 8 中添加 lambda 表达式也会为 Java 中的功能 APICreating 机会。这对于无阻塞的应用程序和延续样式的 API(如CompletableFutureReactiveX所流行)的好处是,它们允许以声明方式构成异步逻辑。在编程模型级别,Java 8 使 Spring WebFlux 能够与带 Comments 的控制器一起提供功能性的 Web 端点。

1.1.1. 定义“Reactive”

我们谈到了“非阻塞性”和“功能性”,但是 Reactive 是什么意思?

术语“Reactive”是指围绕对更改做出反应的编程模型-网络组件对 I/O 事件做出反应,UI 控制器对鼠标事件做出反应等。从这个意义上说,非阻塞是 Reactive 的,因为我们现在正处于操作完成或数据可用时对通知进行反应的方式,而不是被阻塞。

我们 Spring 团队还有另一个重要机制与“Reactive”相关联,这是不阻塞背压的机制。在同步,命令式代码中,阻塞调用是强制调用者 await 的一种自然的背压形式。在非阻塞代码中,控制事件的速率非常重要,这样快速的生产者就不会淹没其目的地。

Reactive Streams 是small spec(在 Java 9 中也是adopted),它定义了具有反压力的异步组件之间的交互。例如,数据存储库(充当Publisher)可以生成 HTTP 服务器(充当Subscriber)然后可以写入响应的数据。 Reactive Streams 的主要目的是让订阅者控制发布者生成数据的速度或速度。

Note

常见问题:发布商不能放慢脚步怎么办?
反应流的目的仅仅是构建机制和边界。如果发布者无法放慢速度,则必须决定是缓冲,删除还是失败。

1.1.2. ReactiveAPI

反应流对于互操作性起着重要作用。库和基础结构组件对此很感兴趣,但由于它太底层了,它作为应用程序 API 的用处不大。应用程序需要更高级别且功能更丰富的 API 来构成异步逻辑,这与 Java 8 Stream API 相似,但不仅适用于集合。这就是反应式库发挥的作用。

Reactor是 Spring WebFlux 的反应库选择。它通过与 ReactiveX 运算符词汇对齐的一组丰富的运算符,提供MonoFlux API 类型,以处理 0..1(Mono)和 0..N(Flux)的数据序列。 Reactor 是 Reactive Streams 库,因此,它的所有运算符都支持无阻塞背压。 Reactor 非常注重服务器端 Java。它是与 Spring 紧密合作开发的。

WebFlux 要求 Reactor 作为核心依赖项,但是它可以通过 Reactive Streams 与其他反应式库互操作。通常,WebFlux API 接受纯Publisher作为 Importing,在内部将其适应于 Reactor 类型,使用该类型,然后返回FluxMono作为输出。因此,您可以传递任何Publisher作为 Importing,并且可以对输出应用操作,但是您需要调整输出以与另一个反应式库一起使用。只要可行(例如,带 Comments 的控制器),WebFlux 就会透明地适应 RxJava 或其他反应式库的使用。有关更多详细信息,请参见Reactive Libraries

1.1.3. 编程模型

spring-web模块包含 Spring WebFlux 基础的反应式基础,包括 HTTP 抽象,用于受支持服务器的 Reactive 流adapterscodecs以及与 Servlet API 类似但具有非阻塞 Contract 的核心WebHandler API

在此基础上,Spring WebFlux 提供了两种编程模型的选择:

  • Annotated Controllers:与 Spring MVC 一致,并基于spring-web模块中的相同 Comments。 Spring MVC 和 WebFlux 控制器都支持反应式(Reactor 和 RxJava)返回类型,因此,区分它们并不容易。一个显着的区别是 WebFlux 还支持 Reactive@RequestBody参数。

  • Functional Endpoints:基于 Lambda 的轻量级功能编程模型。您可以将其视为一个小型库或一组 Util,应用程序可以使用它们来路由和处理请求。带 Comments 的控制器的最大区别在于,应用程序负责从头到尾的请求处理,而不是通过 Comments 声明意图并被回调。

1.1.4. Applicability

Spring MVC 还是 WebFlux?

一个自然的问题要问,但构建了一个不合理的二分法。实际上,两者共同努力扩大了可用选项的范围。两者的设计旨在实现彼此的连续性和一致性,它们可以并行使用,并且双方的反馈对双方都有利。下图显示了两者之间的关系,它们的共同点以及各自的独特支持:

Spring MVC 和 Webflux Venn

我们建议您考虑以下几点:

  • 如果您有运行正常的 Spring MVC 应用程序,则无需更改。命令式编程是编写,理解和调试代码的最简单方法。您有最大的库选择空间,因为从历史上看,大多数库都处于阻塞状态。

  • 如果您已经在购买非阻塞式 Web 堆栈,Spring WebFlux 可以在该领域提供与其他应用程序相同的执行模型优势,还可以选择服务器(Netty,Tomcat,Jetty,Undertow 和 Servlet 3.1 容器)。编程模型(带 Comments 的控制器和功能性 Web 端点),以及反应式库(Reactor,RxJava 或其他)的选择。

  • 如果您对与 Java 8 lambda 或 Kotlin 一起使用的轻量级功能性 Web 框架感兴趣,则可以使用 Spring WebFlux 功能性 Web 端点。对于要求较低复杂性的较小应用程序或微服务(可以受益于更高的透明度和控制)而言,这也是一个不错的选择。

  • 在微服务架构中,您可以混合使用带有 Spring MVC 或 Spring WebFlux 控制器或带有 Spring WebFlux 功能端点的应用程序。在两个框架中都支持相同的基于 Comments 的编程模型,这使得重用知识变得更加容易,同时还为正确的工作选择了正确的工具。

  • 评估应用程序的一种简单方法是检查其依赖关系。如果您要使用阻塞性持久性 API(JPA,JDBC)或网络 API,则 Spring MVC 至少是常见体系结构的最佳选择。使用 Reactor 和 RxJava 在单独的线程上执行阻塞调用在技术上是可行的,但是您不会充分利用非阻塞 Web 堆栈。

  • 如果您的 Spring MVC 应用程序具有对远程服务的调用,请尝试响应式WebClient。您可以直接从 Spring MVC 控制器方法返回反应类型(Reactor,RxJava,or other)。每个呼叫的 await 时间或呼叫之间的相互依赖性越大,好处就越明显。 Spring MVC 控制器也可以调用其他反应式组件。

  • 如果您有庞大的团队,请牢记向无阻塞,功能和声明式编程的过渡过程中的学习曲线很陡。在没有完全切换的情况下启动的实际方法是使用电抗WebClient。除此之外,从小处着手并衡量收益。我们希望,对于广泛的应用程序,这种转变是不必要的。如果不确定要寻找什么好处,请先了解无阻塞 I/O 的工作原理(例如,单线程 Node.js 上的并发性)及其影响。

1.1.5. Servers

Tomcat,Jetty,Servlet 3.1 容器以及非 Servlet 运行时(例如 Netty 和 Undertow)都支持 Spring WebFlux。所有服务器都适用于低级别的common API,因此跨服务器可以支持更高级别的programming models

Spring WebFlux 不具有内置支持来启动或停止服务器。但是,很容易用几行代码从 Spring 配置中assemble一个应用程序和WebFlux infrastructurerun it

Spring Boot 具有一个 WebFlux 启动器,可以自动执行这些步骤。默认情况下,入门者使用 Netty,但通过更改 Maven 或 Gradle 依赖关系,可以轻松切换到 Tomcat,Jetty 或 Undertow。 Spring Boot 默认为 Netty,因为它在异步,非阻塞空间中得到更广泛的使用,并允许 Client 端和服务器共享资源。

Tomcat 和 Jetty 可以与 Spring MVC 和 WebFlux 一起使用。但是请记住,它们的使用方式非常不同。 Spring MVC 依靠 Servlet 阻塞 I/O,并在需要时允许应用程序直接使用 Servlet API。 Spring WebFlux 依赖于 Servlet 3.1 非阻塞 I/O,并在低级适配器后面使用 Servlet API,并且不公开供直接使用。

对于 Undertow,Spring WebFlux 直接使用 Undertow API,而无需使用 Servlet API。

1.1.6. Performance

表演具有许多 Feature 和意义。反应和非阻塞通常不会使应用程序运行得更快。在某些情况下,它们可以(例如,如果使用WebClient并行执行远程调用)。总体而言,以非阻塞方式进行操作需要更多的工作,这可能会稍微增加所需的处理时间。

Reactive 和非阻塞性的主要预期好处是能够以较少的固定数量的线程和较少的内存进行扩展。这使应用程序在负载下更具弹性,因为它们以更可预测的方式扩展。但是,为了观察这些好处,您需要有一些延迟(包括缓慢的和不可预测的网络 I/O)。这就是反应堆开始显示其优势的地方,差异可能很大。

1.1.7. 并发模型

Spring MVC 和 Spring WebFlux 都支持带 Comments 的控制器,但是并发模型和默认的阻塞和线程假设存在关键差异。

在 Spring MVC(通常是 servlet 应用程序)中,假定应用程序可以阻塞当前线程(例如,用于远程调用),因此,servlet 容器使用大线程池来吸收请求期间的潜在阻塞。处理。

在 Spring WebFlux(通常是非阻塞服务器)中,假定应用程序未阻塞,因此,非阻塞服务器使用固定大小的小型线程池(事件循环工作器)来处理请求。

Tip

“按比例缩放”和“少量线程”听起来可能是矛盾的,但是从不阻塞当前线程(而是依赖于回调)意味着您不需要额外的线程,因为没有阻塞调用可以吸收。

调用阻止 API

如果确实需要使用阻止库怎么办? Reactor 和 RxJava 都提供publishOn运算符以 continue 在其他线程上进行处理。这意味着容易逃生。但是请记住,阻塞式 API 不适用于此并发模型。

Mutable State

在 Reactor 和 RxJava 中,您可以通过运算符声明逻辑,然后在运行时形成反应式管道,在该管道中,数据在不同的阶段被 Sequences 处理。这样做的主要好处是,它使应用程序不必保护可变状态,因为该管道中的应用程序代码永远不会被并发调用。

Threading Model

您期望在运行 Spring WebFlux 的服务器上看到哪些线程?

  • 在“原始” Spring WebFlux 服务器上(例如,没有数据访问权限或其他可选依赖项),您可以期望该服务器有一个线程,而其他几个线程则可以进行请求处理(通常与 CPU 核心数量一样多)。但是,Servlet 容器可能以更多线程开始(例如,Tomcat 上为 10),以支持 Servlet(阻塞)I/O 和 Servlet 3.1(非阻塞)I/O 使用。

  • 反应式WebClient以事件循环样式运行。因此,您可以看到与之相关的固定数量的处理线程(例如,带有 Reactor Netty 连接器的reactor-http-nio-)。但是,如果 Client 端和服务器都使用 Reactor Netty,则默认情况下两者共享事件循环资源。

  • Reactor 和 RxJava 提供了称为调度程序的线程池抽象,以与publishOn运算符配合使用,该运算符用于将处理切换到其他线程池。调度程序具有建议特定并发策略的名称-例如,“并行”(对于具有有限数量的线程的 CPU 绑定工作)或“弹性”(对于具有大量线程的 I/O 绑定)。如果看到这样的线程,则意味着某些代码正在使用特定的线程池Scheduler策略。

  • 数据访问库和其他第三方依赖性也可以创建和使用自己的线程。

Configuring

Spring 框架不支持启动和停止servers。要为服务器配置线程模型,您需要使用服务器特定的配置 API,或者,如果使用 Spring Boot,请检查每个服务器的 Spring Boot 配置选项。您可以直接configure WebClient。对于所有其他库,请参阅其各自的文档。

1.2. 反应堆芯

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

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

  • HttpHandler:具有非阻塞 I/O 和响应流反压力的 HTTP 请求处理的基本协定,以及 Reactor Netty,Undertow,Tomcat,Jetty 和任何 Servlet 3.1 容器的适配器。

    • WebHandler API:用于请求处理的更高级别的通用 Web API,在此之上构建了具体的编程模型,例如带 Comments 的控制器和功能端点。
  • 对于 Client 端,有一个基本的ClientHttpConnector协定,以执行具有非阻塞 I/O 和响应流反压力的 HTTP 请求,以及用于Reactor Netty和响应Jetty HtpClient的适配器。应用程序中使用的较高级别WebClient以此基本 Contract 为基础。

  • 对于 Client 端和服务器,codecs用于对 HTTP 请求和响应内容进行序列化和反序列化。

1.2.1. HttpHandler

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

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

Server name 使用的服务器 API 反应式流支持
Netty Netty API Reactor Netty
Undertow Undertow API spring-web:向响应流 bridge 过渡
Tomcat Servlet 3.1 非阻塞 I/O; Tomcat API 读写 ByteBuffers 与 byte [] spring-web:Servlet 3.1 非阻塞 I/O 到响应流 bridge
Jetty Servlet 3.1 非阻塞 I/O; Jetty API 编写 ByteBuffers 与 byte [] spring-web:Servlet 3.1 非阻塞 I/O 到响应流 bridge
Servlet 3.1 容器 Servlet 3.1 非阻塞 I/O spring-web:Servlet 3.1 非阻塞 I/O 到响应流 bridge

下表描述了服务器依赖性(另请参见supported versions):

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
Jetty org.eclipse.jetty jetty-server, jetty-servlet

下面的代码段显示了在每个服务器 API 中使用HttpHandler适配器:

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 容器

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

1.2.2. WebHandler API

org.springframework.web.server包构建在HttpHandlerContracts 的基础上,以提供通用 Web API,以通过多个WebExceptionHandler,多个WebFilter和单个WebHandler组件链处理请求。可以通过简单地指向组件为auto-detected的 Spring ApplicationContext和/或通过向构建器注册组件来将链与WebHttpHandlerBuilder放在一起。

HttpHandler的抽象目标很简单,而WebHandler API 的目的是提供 Web 应用程序中常用的更广泛的功能集,例如:

  • 具有属性的用户会话。

  • Request attributes.

  • 解决了LocalePrincipal的请求。

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

  • Multipart 数据的抽象。

  • and more..

特殊 bean 类型

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

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

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

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

DefaultServerWebExchange使用配置的HttpMessageReader将表单数据(application/x-www-form-urlencoded)解析为MultiValueMap。缺省情况下,FormHttpMessageReader配置为由ServerCodecConfigurer bean 使用(请参见Web 处理程序 API)。

Multipart Data

与 Spring MVC 中的相同

ServerWebExchange公开了以下访问 Multipart 数据的方法:

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

DefaultServerWebExchange使用配置的HttpMessageReader<MultiValueMap<String, Part>>multipart/form-data内容解析为MultiValueMap。目前,Synchronoss NIOMultipart是唯一受支持的第三方库,也是我们知道的用于非阻塞解析 Multipart 请求的唯一库。通过ServerCodecConfigurer bean(请参阅Web 处理程序 API)启用了它。

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

Forwarded Headers

与 Spring MVC 中的相同

当请求通过代理(例如负载平衡器)进行处理时,主机,端口和方案可能会更改,从 Client 端的角度来看,要创建指向正确的主机,端口和方案的链接是一个挑战。

RFC 7239定义Forwarded HTTPHeaders,代理可用来提供有关原始请求的信息。还有其他非标准 Headers,包括X-Forwarded-HostX-Forwarded-PortX-Forwarded-ProtoX-Forwarded-SslX-Forwarded-Prefix

ForwardedHeaderTransformer是一个组件,可根据转发的 Headers 修改请求的主机,端口和方案,然后删除这些 Headers。您可以将其声明为名称为forwardedHeaderTransformer的 bean,并且它是detected并且已使用。

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

Note

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

1.2.3. Filters

与 Spring MVC 中的相同

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

CORS

与 Spring MVC 中的相同

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

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

1.2.4. Exceptions

与 Spring MVC 中的相同

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

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

Exception Handler Description
ResponseStatusExceptionHandler 通过将响应设置为异常的 HTTP 状态代码,提供对ResponseStatusException类型的异常的处理。
WebFluxResponseStatusExceptionHandler ResponseStatusExceptionHandlerextensions,也可以确定任何异常上的@ResponseStatus注解的 HTTP 状态代码。


该处理程序在WebFlux Config中声明。

1.2.5. Codecs

与 Spring MVC 中的相同

spring-webspring-core模块提供了对通过非阻塞 I/O(具有 Reactive Streams 背压)在高级对象之间来回串行化和反序列化字节内容的支持。以下介绍了此支持:

  • 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,每个_代表一个 JSON 对象。

  • 每个TokenBuffer都传递给 Jackson 的ObjectMapper以创建更高级别的对象。

  • 解码为单值发布者(例如Mono)时,有一个TokenBuffer

  • 当解码为多值发布者(例如Flux)时,只要为完整格式的对象接收到足够的字节,每个TokenBuffer就会传递给ObjectMapper。Importing 的内容可以是 JSON 数组,如果 Content Type 为“ application/stream json”,则为line-delimited JSON

Jackson2Encoder的工作方式如下:

  • 对于单个价值发布者(例如Mono),只需通过ObjectMapper对其进行序列化即可。

  • 对于具有“ application/json”的多值发布者,默认情况下使用Flux#collectToList()收集值,然后序列化结果集合。

  • 对于具有流媒体类型(例如application/stream+jsonapplication/stream+x-jackson-smile)的多值发布者,请使用line-delimited JSON格式分别对每个值进行编码,写入和刷新。

  • 对于 SSE,每个事件都调用Jackson2Encoder,并且刷新输出以确保传递时没有延迟。

Note

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

Form Data

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

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

使用getFormData()后,将无法再从请求正文中读取原始原始内容。因此,与从原始请求主体读取数据相比,应用程序应始终通过ServerWebExchange访问缓存的表单数据。

Multipart

MultipartHttpMessageReaderMultipartHttpMessageWriter支持对“Multipart/表单数据”内容进行解码和编码。依次将MultipartHttpMessageReader委派给另一个HttpMessageReader以便实际解析为Flux<Part>,然后将这些部分简单地收集到MultiValueMap中。目前,Synchronoss NIOMultipart用于实际解析。

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

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

Streaming

与 Spring MVC 中的相同

在流式传输到 HTTP 响应(例如text/event-streamapplication/stream+json)时,定期发送数据很重要,以便尽早而不是稍后可靠地检测到断开连接的 Client 端。这样的发送可以是仅 Comments 的空 SSE 事件,也可以是有效用作心跳的任何其他“无操作”数据。

DataBuffer

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

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

1.2.6. Logging

与 Spring MVC 中的相同

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

与 Spring MVC 中的相同

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

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

@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {

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

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

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

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

1.3. DispatcherHandler

与 Spring MVC 中的相同

与 Spring MVC 类似,Spring WebFlux 围绕前端控制器模式进行设计,其中中央WebHandlerDispatcherHandler提供了用于请求处理的共享算法,而实际工作是由可配置的委托组件执行的。该模型非常灵活,并支持多种工作流程。

DispatcherHandler从 Spring 配置中发现所需的委托组件。它还被设计为 Spring Bean 本身,并实现ApplicationContextAware以访问其运行的上下文。如果以webHandler的 bean 名称声明了DispatcherHandler,则依次由WebHttpHandlerBuilder发现,该_组合了一个请求处理链,如WebHandler API中所述。

WebFlux 应用程序中的 Spring 配置通常包含:

配置已分配给WebHttpHandlerBuilder以构建处理链,如以下示例所示:

ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context);

生成的HttpHandler准备与server adapter一起使用。

1.3.1. 特殊 bean 类

与 Spring MVC 中的相同

DispatcherHandler委托特殊 bean 处理请求并呈现适当的响应。所谓“特殊 bean”,是指实现 WebFlux 框架 Contract 的 SpringManagement 的Object实例。这些通常带有内置 Contract,但是您可以自定义它们的属性,扩展它们或替换它们。

下表列出了DispatcherHandler检测到的特殊 bean。请注意,在较低级别还检测到其他一些 Bean(请参阅 Web Handler API 中的特殊 bean 类)。

Bean type Explanation
HandlerMapping 将请求 Map 到处理程序。Map 基于某些条件,具体细节因HandlerMapping实现方式(带 Comments 的控制器,简单的 URL 模式 Map 等)而异。


HandlerMapping主要实现是_用于@RequestMapping带 Comments 的方法,RouterFunctionMapping用于功能性端点路由,SimpleUrlHandlerMapping用于 URI 路径模式和WebHandler实例的显式注册。
| HandlerAdapter |帮助DispatcherHandler调用 Map 到请求的处理程序,而不管该处理程序的实际调用方式如何。例如,调用带 Comments 的控制器需要解析 Comments。 HandlerAdapter的主要目的是使DispatcherHandler免受此类细节的影响。
| HandlerResultHandler |处理来自处理程序调用的结果并最终确定响应。参见Result Handling

1.3.2. WebFlux 配置

与 Spring MVC 中的相同

应用程序可以声明处理请求所需的基础结构 bean(在Web 处理程序 APIDispatcherHandler下列出)。但是,在大多数情况下,WebFlux Config是最佳起点。它声明了所需的 bean,并提供了更高级别的配置回调 API 来对其进行自定义。

Note

Spring Boot 依靠 WebFlux 配置来配置 Spring WebFlux,并且还提供了许多额外的方便选项。

1.3.3. Processing

与 Spring MVC 中的相同

DispatcherHandler处理请求的方式如下:

  • 要求每个HandlerMapping查找匹配的处理程序,并使用第一个匹配项。

  • 如果找到处理程序,则通过适当的HandlerAdapter执行该处理程序,该处理程序将执行返回的值公开为HandlerResult

  • 通过直接写入响应或使用视图进行渲染,将HandlerResult赋予适当的HandlerResultHandler以完成处理。

1.3.4. 结果处理

通过HandlerAdapter调用处理程序的返回值与HandlerResult一起包装为HandlerResult,并附加到其他上下文中,并传递给要求支持它的第一个HandlerResultHandler。下表显示了可用的HandlerResultHandler实现,所有实现均在WebFlux Config中声明:

结果处理程序类型 Return Values Default Order
ResponseEntityResultHandler ResponseEntity,通常来自@Controller个实例。 0
ServerResponseResultHandler ServerResponse,通常来自功能端点。 0
ResponseBodyResultHandler 处理来自@ResponseBody个方法或@RestController个类的返回值。 100
ViewResolutionResultHandler CharSequenceViewModelMapRendering或任何其他Object被视为模型属性。

另请参见View ResolutionInteger.MAX_VALUE

1.3.5. Exceptions

与 Spring MVC 中的相同

HandlerAdapter返回的HandlerResult可以公开基于某些特定于处理程序的机制进行错误处理的函数。在以下情况下将调用此错误函数:

  • 处理程序(例如@Controller)调用失败。

  • 通过HandlerResultHandler处理处理程序返回值失败。

只要在从处理程序返回的反应类型产生任何数据项之前发生错误 signal,错误函数就可以更改响应(例如,更改为错误状态)。

这就是支持@Controller类中的@ExceptionHandler方法的方式。相比之下,Spring MVC 中对HandlerExceptionResolver的支持基于此。这通常不重要。但是,请记住,在 WebFlux 中,不能使用@ControllerAdvice处理在选择处理程序之前发生的异常。

另请参见“带 Comments 的控制器”部分中的Managing Exceptions或 WebHandler API 部分中的Exceptions

1.3.6. 查看分辨率

与 Spring MVC 中的相同

视图分辨率使您可以使用 HTML 模板和模型渲染到浏览器,而无需将您与特定的视图技术联系在一起。在 Spring WebFlux 中,通过使用ViewResolver实例将 String(代表逻辑视图名称)Map 到View实例的专用HandlerResultHandler支持视图解析。 View然后用于呈现响应。

Handling

与 Spring MVC 中的相同

传递给ViewResolutionResultHandlerHandlerResult包含处理程序的返回值和包含请求处理过程中添加的属性的模型。返回值将作为以下值之一进行处理:

  • StringCharSequence:通过已配置的ViewResolver实现的列表解析为View的逻辑视图名称。

  • void:根据请求路径选择默认视图名称,减去前斜杠和后斜杠,然后将其解析为View。当未提供视图名称(例如,返回了模型属性)或异步返回值(例如,Mono已完成为空)时,也会发生同样的情况。

  • Rendering:用于视图分辨率方案的 API。通过代码完成探索 IDE 中的选项。

  • ModelMap:要添加到请求模型的额外模型属性。

  • 任何其他:任何其他返回值(由BeanUtils#isSimpleProperty确定的简单类型除外)都将被视为要添加到模型的模型属性。除非存在处理程序方法@ModelAttribute注解,否则使用conventions从类名称派生属性名称。

该模型可以包含异步,反应式类型(例如,来自 Reactor 或 RxJava)。在渲染之前,AbstractView将此类模型属性解析为具体值并更新模型。单值反应类型被解析为单个值或无值(如果为空),而多值反应类型(例如Flux<T>)被收集并解析为List<T>

配置视图分辨率就像在 Spring 配置中添加ViewResolutionResultHandler bean 一样简单。 WebFlux Config提供用于视图分辨率的专用配置 API。

有关与 Spring WebFlux 集成的视图技术的更多信息,请参见View Technologies

Redirecting

与 Spring MVC 中的相同

视图名称中特殊的redirect:前缀使您可以执行重定向。 UrlBasedViewResolver(及其子类)将其识别为需要重定向的指令。视图名称的其余部分是重定向 URL。

最终效果与控制器返回RedirectViewRendering.redirectTo("abc").build()的效果相同,但是现在控制器本身可以根据逻辑视图名称进行操作。诸如redirect:/some/resource之类的视图名称是相对于当前应用程序的,而诸如redirect:http://example.com/arbitrary/path之类的视图名称则重定向到绝对 URL。

Content Negotiation

与 Spring MVC 中的相同

ViewResolutionResultHandler支持内容协商。它将请求媒体类型与每个选定的View支持的媒体类型进行比较。使用支持请求的媒体类型的第一个View

为了支持 JSON 和 XML 之类的媒体类型,Spring WebFlux 提供了HttpMessageWriterView,这是一个特殊的View,它通过HttpMessageWriter呈现。通常,您可以通过WebFlux Configuration将它们配置为默认视图。如果默认视图与请求的媒体类型匹配,则始终会选择和使用它们。

1.4. 带 Comments 的控制器

与 Spring MVC 中的相同

Spring WebFlux 提供了一个基于 Comments 的编程模型,其中@Controller@RestController组件使用 Comments 来表达请求 Map,请求 Importing,处理异常等。带 Comments 的控制器具有灵活的方法签名,无需扩展 Base Class 或实现特定的接口。

以下 Lists 显示了一个基本示例:

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String handle() {
        return "Hello WebFlux";
    }
}

在前面的示例中,该方法返回要写入响应主体的String

1.4.1. @Controller

与 Spring MVC 中的相同

您可以使用标准的 Spring bean 定义来定义控制器 bean。 @Controller原型允许自动检测,并且与 Spring 常规支持保持一致,以支持在 Classpath 中检测@Component类并为其自动注册 Bean 定义。它还充当带 Comments 类的构造型,表明其作为 Web 组件的作用。

要启用对此类@Controller bean 的自动检测,可以将组件扫描添加到 Java 配置中,如以下示例所示:

@Configuration
@ComponentScan("org.example.web") (1)
public class WebConfig {

    // ...
}
  • (1) 扫描org.example.web软件包。

@RestController是本身由@Controller@ResponseBody进行元 Comments 的composed annotation,表示其每个方法都继承了类型级别@ResponseBodyComments 的控制器,因此直接将其写入响应主体(与视图分辨率和 HTML 模板渲染相比)。

1.4.2. 请求 Map

与 Spring MVC 中的相同

@RequestMapping注解用于将请求 Map 到控制器方法。它具有各种属性,可以通过 URL,HTTP 方法,请求参数,Headers 和媒体类型进行匹配。您可以在类级别使用它来表示共享的 Map,也可以在方法级别使用它来缩小到特定的端点 Map。

也有@RequestMapping的 HTTP 方法特定的快捷方式:

  • @GetMapping

  • @PostMapping

  • @PutMapping

  • @DeleteMapping

  • @PatchMapping

提供前面的 Comments 是Custom Annotations,因为可以说,大多数控制器方法应该 Map 到特定的 HTTP 方法,而不是使用@RequestMapping,默认情况下,@RequestMapping匹配所有 HTTP 方法。同时,在类级别仍需要@RequestMapping来表示共享 Map。

以下示例使用类型和方法级别的 Map:

@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    public Person getPerson(@PathVariable Long id) {
        // ...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        // ...
    }
}
URI Patterns

与 Spring MVC 中的相同

您可以使用全局模式和通配符来 Map 请求:

  • ?匹配一个字符

  • *匹配路径段中的零个或多个字符

  • **匹配零个或多个路径段

您还可以声明 URI 变量并使用@PathVariable访问其值,如以下示例所示:

@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
    // ...
}

您可以在类和方法级别声明 URI 变量,如以下示例所示:

@Controller
@RequestMapping("/owners/{ownerId}") (1)
public class OwnerController {

    @GetMapping("/pets/{petId}") (2)
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        // ...
    }
}
  • (1) 类级 URIMap。
  • (2) 方法级 URIMap。

URI 变量会自动转换为适当的类型,或者引发TypeMismatchException。默认情况下支持简单类型(intlongDate等),您可以注册对任何其他数据类型的支持。参见Type ConversionDataBinder

URI 变量可以显式命名(例如@PathVariable("customId")),但是如果名称相同,则可以省略该详细信息,并使用调试信息或 Java 8 上的-parameters编译器标志编译代码。

语法{*varName}声明了一个与零个或多个剩余路径段匹配的 URI 变量。例如,/resources/{*path}匹配所有文件/resources/,并且"path"变量捕获完整的相对路径。

语法{varName:regex}声明带有正则表达式的 URI 变量,语法为{varName:regex}。例如,给定 URL /spring-web-3.0.5 .jar,以下方法将提取名称,版本和文件 extensions:

@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
    // ...
}

URI 路径模式也可以嵌入${…}占位符,这些占位符在启动时通过PropertyPlaceHolderConfigurer会针对本地,系统,环境和其他属性源进行解析。您可以使用它来例如基于某些外部配置参数化基本 URL。

Note

Spring WebFlux 使用PathPatternPathPatternParser来获得 URI 路径匹配支持。这两个类都位于spring-web中,并且专门设计用于 Web 应用程序中的 HTTP URL 路径,在 Web 应用程序中,在运行时会匹配大量 URI 路径模式。

Spring WebFlux 不支持后缀模式匹配-与 Spring MVC 不同,在 Spring MVC 中,诸如/person的 Map 也匹配到/person.*。对于基于 URL 的内容协商,如果需要,我们建议使用查询参数,该参数更简单,更明确,并且不易受到基于 URL 路径的攻击。

Pattern Comparison

与 Spring MVC 中的相同

当多个模式与 URL 匹配时,必须将它们进行比较以找到最佳匹配。这是通过PathPattern.SPECIFICITY_COMPARATOR完成的,该PathPattern.SPECIFICITY_COMPARATOR查找更具体的模式。

对于每个模式,都会根据 URI 变量和通配符的数量计算得分,其中 URI 变量的得分低于通配符。总得分较低的模式将获胜。如果两个模式的分数相同,则选择更长的时间。

包罗万象的模式(例如**{*varName})不计入评分,而是始终排在最后。如果两种模式都适用,则选择较长的模式。

消耗媒体类型

与 Spring MVC 中的相同

您可以根据请求的Content-Type缩小请求 Map,如下例所示:

@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
    // ...
}

消耗属性还支持否定表达式-例如,!text/plain表示text/plain以外的任何 Content Type。

您可以在类级别声明共享的consumes属性。但是,与大多数其他请求 Map 属性不同,在类级别使用时,方法级别的consumes属性会覆盖而不是扩展类级别的声明。

Tip

MediaType提供常用媒体类型的常量,例如APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE

可生产的媒体类型

与 Spring MVC 中的相同

您可以根据Accept请求 Headers 和控制器方法生成的 Content Type 列表来缩小请求 Map,如下例所示:

@GetMapping(path = "/pets/{petId}", produces = "application/json;charset=UTF-8")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    // ...
}

媒体类型可以指定字符集。支持否定的表达式。例如,!text/plain表示text/plain以外的任何 Content Type。

Note

对于 JSONContent Type,即使RFC7159明确指出“未为此注册定义任何字符集参数”,也应指定 UTF-8 charset,因为某些浏览器要求它正确解释 UTF-8 特殊字符。

您可以在类级别声明共享的produces属性。但是,与大多数其他请求 Map 属性不同,在类级别使用时,方法级别的produces属性会覆盖而不是扩展类级别的声明。

Tip

MediaType提供常用媒体类型的常量,例如。 APPLICATION_JSON_UTF8_VALUEAPPLICATION_XML_VALUE

参数和标题

与 Spring MVC 中的相同

您可以根据查询参数条件来缩小请求 Map。您可以测试是否存在查询参数(myParam),查询参数是否不存在(!myParam)或特定值(myParam=myValue)。以下示例测试具有值的参数:

@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
  • (1) 检查myParam等于myValue

您还可以将其与请求 Headers 条件一起使用,如以下示例所示:

@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
  • (1) 检查myHeader等于myValue
HTTP HEAD,选项

与 Spring MVC 中的相同

@GetMapping@RequestMapping(method=HttpMethod.GET)透明地支持 HTTP HEAD,以进行请求 Map。控制器方法无需更改。 HttpHandler服务器适配器中应用的响应包装器可确保将Content-LengthHeaders 设置为写入的字节数,而无需实际写入响应。

默认情况下,通过将Allow响应 Headers 设置为所有具有匹配 URL 模式的@RequestMapping方法中列出的 HTTP 方法列表来处理 HTTP OPTIONS。

对于没有 HTTP 方法声明的@RequestMappingAllowHeaders 设置为GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS。控制器方法应始终声明支持的 HTTP 方法(例如,使用 HTTP 方法特定的变体@GetMapping@PostMapping等)。

您可以将@RequestMapping方法显式 Map 到 HTTP HEAD 和 HTTP OPTIONS,但这在通常情况下不是必需的。

Custom Annotations

与 Spring MVC 中的相同

Spring WebFlux 支持使用composed annotations进行请求 Map。这些注解本身用@RequestMapping进行元注解,并组成它们以更狭窄,更具体的用途重新声明@RequestMapping属性的子集(或全部)。

@GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping是组合 Comments 的示例。之所以提供它们,是因为大多数控制器方法应该 Map 到特定的 HTTP 方法,而不是使用@RequestMapping,默认情况下,@RequestMapping匹配所有 HTTP 方法。如果需要组合 Comments 的示例,请查看如何声明它们。

Spring WebFlux 还支持具有自定义请求匹配逻辑的自定义请求 Map 属性。这是一个更高级的选项,它需要子类化RequestMappingHandlerMapping并覆盖getCustomMethodCondition方法,您可以在其中检查 custom 属性并返回自己的RequestCondition

Explicit Registrations

与 Spring MVC 中的相同

您可以以编程方式注册 Handler 方法,这些方法可用于动态注册或高级用例,例如同一处理程序在不同 URL 下的不同实例。以下示例显示了如何执行此操作:

@Configuration
public class MyConfig {

    @Autowired
    public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
            throws NoSuchMethodException {

        RequestMappingInfo info = RequestMappingInfo
                .paths("/user/{id}").methods(RequestMethod.GET).build(); (2)

        Method method = UserHandler.class.getMethod("getUser", Long.class); (3)

        mapping.registerMapping(info, handler, method); (4)
    }

}
  • (1) 注入目标处理程序和控制器的处理程序 Map。
  • (2) 准备请求 Map 元数据。
  • (3) 获取处理程序方法。
  • (4) 添加注册。

1.4.3. 处理程序方法

与 Spring MVC 中的相同

@RequestMapping处理程序方法具有灵活的签名,可以从一系列受支持的控制器方法参数和返回值中进行选择。

Method Arguments

与 Spring MVC 中的相同

下表显示了受支持的控制器方法参数。

需要解析 I/O(例如,读取请求正文)的参数支持 Reactive 类型(Reactor,RxJava,or other)。这在“描述”列中进行了标记。不需要阻塞的参数不应使用 Reactive 类型。

支持 JDK 1.8 的java.util.Optional作为方法参数,并与具有required属性(例如@RequestParam@RequestHeader等)的注解结合使用,并且与required=false等效。

控制器方法参数 Description
ServerWebExchange 访问完整的ServerWebExchange容器,以获取 HTTP 请求和响应,请求和会话属性,checkNotModified方法等。
ServerHttpRequest , ServerHttpResponse 访问 HTTP 请求或响应。
WebSession 访问会话。除非添加了属性,否则这不会强制开始新的会话。支持反应类型。
java.security.Principal 当前经过身份验证的用户-可能是特定的Principal实现类(如果已知)。支持反应类型。
org.springframework.http.HttpMethod 请求的 HTTP 方法。
java.util.Locale 当前的请求语言环境,实际上是由最具体的LocaleResolver确定的,即已配置的LocaleResolver/LocaleContextResolver
java.util.TimeZone + java.time.ZoneId 与当前请求关联的时区,由LocaleContextResolver确定。
@PathVariable 用于访问 URI 模板变量。参见URI Patterns
@MatrixVariable 用于访问 URI 路径段中的名称/值对。参见Matrix Variables
@RequestParam 用于访问 Servlet 请求参数。参数值将转换为声明的方法参数类型。参见@RequestParam


请注意,使用@RequestParam是可选的,例如用于设置其属性。请参阅此表后面的“其他任何参数”。
| @RequestHeader |用于访问请求 Headers。Headers 值将转换为声明的方法参数类型。参见@RequestHeader
| @CookieValue |用于访问 cookie。 Cookie 值将转换为声明的方法参数类型。参见@CookieValue
| @RequestBody |用于访问 HTTP 请求正文。正文内容通过使用HttpMessageReader实例转换为声明的方法参数类型。支持反应类型。参见@RequestBody
| HttpEntity<B> |用于访问请求 Headers 和正文。主体使用HttpMessageReader个实例进行转换。支持反应类型。参见HttpEntity
| @RequestPart |用于访问multipart/form-data请求中的 Component。支持反应类型。参见Multipart ContentMultipart Data
| java.util.Maporg.springframework.ui.Modelorg.springframework.ui.ModelMap。|用于访问 HTML 控制器中使用的模型,并作为视图渲染的一部分暴露给模板。
| @ModelAttribute |用于访问应用了数据绑定和验证的模型中的现有属性(如果不存在,则进行实例化)。参见@ModelAttribute以及ModelDataBinder
请注意,使用@ModelAttribute是可选的,例如用于设置其属性。请参阅此表后面的“其他任何参数”。
| ErrorsBindingResult |用于访问验证和命令对象(即@ModelAttribute自变量)的数据绑定错误或@RequestBody@RequestPart自变量验证的错误。必须在经过验证的方法参数后立即声明ErrorsBindingResult参数。
| SessionStatus类级@SessionAttributes |用于标记表单处理完成,将触发清除通过类级@SessionAttributesComments 声明的会话属性。有关更多详细信息,请参见@SessionAttributes
| UriComponentsBuilder |用于准备相对于当前请求的主机,端口,方案和路径的 URL。参见URI Links
| @SessionAttribute |用于访问任何会话属性-与由于类级别@SessionAttributes声明而存储在会话中的模型属性相反。有关更多详细信息,请参见@SessionAttribute
| @RequestAttribute |用于访问请求属性。有关更多详细信息,请参见@RequestAttribute
|任何其他自变量|如果方法自变量与以上任何一个都不匹配,则默认情况下,如果它是由BeanUtils#isSimpleProperty确定的简单类型,则默认解析为@RequestParam;否则,解析为@ModelAttribute

Return Values

与 Spring MVC 中的相同

下表显示了受支持的控制器方法返回值。请注意,所有返回值通常都支持 Reactor,RxJava,or other之类的库中的反应类型。

控制器方法返回值 Description
@ResponseBody 返回值通过HttpMessageWriter个实例进行编码,并写入响应中。参见@ResponseBody
HttpEntity<B> , ResponseEntity<B> 返回值指定完整的响应,包括 HTTPHeaders,并且正文通过HttpMessageWriter实例进行编码并写入响应中。参见ResponseEntity
HttpHeaders 用于返回不包含标题的响应。
String ViewResolver实例解析并与隐式模型一起使用的视图名称(通过命令对象和@ModelAttribute方法确定)。处理程序方法还可以pass 语句Model参数(描述为earlier)以编程方式丰富模型。
View 用于与隐式模型一起渲染的View实例-通过命令对象和@ModelAttribute方法确定。处理程序方法还可以pass 语句Model参数(描述为earlier)以编程方式丰富模型。
java.util.Map , org.springframework.ui.Model 要添加到隐式模型的属性,视图名称根据请求路径隐式确定。
@ModelAttribute 要添加到模型的属性,视图名称根据请求路径隐式确定。


请注意,@ModelAttribute是可选的。请参阅本表后面的“其他任何返回值”。
| Rendering |用于模型和视图渲染方案的 API。
| void |具有void,可能是异步的方法(例如Mono<Void>),返回类型(或null返回值)的方法,如果它也具有ServerHttpResponseServerWebExchange参数或@ResponseStatusComments,则认为已完全处理了响应。如果控制器对 ETag 或lastModified时间戳进行了肯定检查,则也是如此。 // TODO:有关详细信息,请参见Controllers
如果以上所有条件都不成立,则void返回类型还可以为 REST 控制器指示“无响应正文”,或者为 HTML 控制器指示默认视图名称选择。
| Flux<ServerSentEvent>Observable<ServerSentEvent>或其他响应式类型|发送服务器发送的事件。当仅需要写入数据时,可以省略ServerSentEvent包装器(但是,必须通过produces属性在 Map 中请求或声明text/event-stream)。
|任何其他返回值|如果返回值与以上任何一个都不匹配,则默认情况下将其视为视图名称,如果是Stringvoid(适用默认视图名称选择)或模型属性除非它是简单的类型(如BeanUtils#isSimpleProperty所确定),否则将其添加到模型中,在这种情况下,它仍未解析。

Type Conversion

与 Spring MVC 中的相同

如果参数声明为String以外的其他内容,则表示基于字符串的请求 Importing 的某些带 Comments 的控制器方法参数(例如@RequestParam@RequestHeader@PathVariable@MatrixVariable@CookieValue)可能需要类型转换。

在这种情况下,将根据配置的转换器自动应用类型转换。默认情况下,支持简单类型(例如intlongDate等)。可以通过WebDataBinder(请参见[mvc-ann-initbinder])或通过向FormattingConversionService注册Formatters(请参见Spring 字段格式)来自定义类型转换。

Matrix Variables

与 Spring MVC 中的相同

RFC 3986讨论路径段中的名称/值对。在 Spring WebFlux 中,基于 Tim Berners-Lee 的"old post",我们将其称为“矩阵变量”,但它们也可以称为 URI 路径参数。

矩阵变量可以出现在任何路径段中,每个变量用分号分隔,多个值用逗号分隔,例如"/cars;color=red,green;year=2012"。也可以通过重复的变量名来指定多个值,例如"color=red;color=green;color=blue"

与 Spring MVC 不同,在 WebFlux 中,URL 中是否存在矩阵变量不会影响请求 Map。换句话说,您不需要使用 URI 变量来屏蔽变量内容。就是说,如果要从控制器方法访问矩阵变量,则需要将 URI 变量添加到期望矩阵变量的路径段中。以下示例显示了如何执行此操作:

// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {

    // petId == 42
    // q == 11
}

鉴于所有路径段都可以包含矩阵变量,因此有时可能需要消除矩阵变量应位于哪个路径变量的歧义,如以下示例所示:

// GET /owners/42;q=11/pets/21;q=22

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable(name="q", pathVar="ownerId") int q1,
        @MatrixVariable(name="q", pathVar="petId") int q2) {

    // q1 == 11
    // q2 == 22
}

您可以定义一个矩阵变量,可以将其定义为可选变量并指定一个默认值,如以下示例所示:

// GET /pets/42

@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {

    // q == 1
}

要获取所有矩阵变量,请使用MultiValueMap,如以下示例所示:

// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable MultiValueMap<String, String> matrixVars,
        @MatrixVariable(pathVar="petId"") MultiValueMap<String, String> petMatrixVars) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 22, "s" : 23]
}
@RequestParam

与 Spring MVC 中的相同

您可以使用@RequestParam注解将查询参数绑定到控制器中的方法参数。以下代码段显示了用法:

@Controller
@RequestMapping("/pets")
public class EditPetForm {

    // ...

    @GetMapping
    public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute("pet", pet);
        return "petForm";
    }

    // ...

}
  • (1) 使用@RequestParam

Tip

Servlet API 的“请求参数”概念将查询参数,表单数据和 Multipart 合并为一个。但是,在 WebFlux 中,每个对象都是通过ServerWebExchange单独访问的。虽然@RequestParam仅绑定到查询参数,但是您可以使用数据绑定将查询参数,表单数据和 Multipart 应用于command object

默认情况下,使用@RequestParamComments 的方法参数是必需的,但是您可以通过将@RequestParam的必需标志设置为false或通过java.util.Optional包装器声明该参数来指定方法参数是可选的。

如果目标方法参数类型不是String,则会自动应用类型转换。参见[mvc-ann-typeconversion]

Map<String, String>MultiValueMap<String, String>参数上声明@RequestParamComments 时,将使用所有查询参数填充 Map。

请注意,使用@RequestParam是可选的,例如用于设置其属性。默认情况下,任何简单值类型(由BeanUtils#isSimpleProperty确定)且未被其他任何参数解析器解析的参数都将被视为已用@RequestParamComments。

@RequestHeader

与 Spring MVC 中的相同

您可以使用@RequestHeader注解将请求 Headers 绑定到控制器中的方法参数。

以下示例显示了带有 Headers 的请求:

Host                    localhost:8080
Accept                  text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language         fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding         gzip,deflate
Accept-Charset          ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive              300

下面的示例获取Accept-EncodingKeep-AliveHeaders 的值:

@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, (1)
        @RequestHeader("Keep-Alive") long keepAlive) { (2)
    //...
}
  • (1) 获取Accept-EncogingHeaders 的值。
  • (2) 获取Keep-AliveHeaders 的值。

如果目标方法参数类型不是String,则会自动应用类型转换。参见[mvc-ann-typeconversion]

Map<String, String>MultiValueMap<String, String>HttpHeaders参数上使用@RequestHeaderComments 时,将使用所有 Headers 值填充 Map。

Tip

内置支持可用于将逗号分隔的字符串转换为数组或字符串集合或类型转换系统已知的其他类型。例如,带有@RequestHeader("Accept")Comments 的方法参数可以是String类型,也可以是String[]List<String>类型。

@CookieValue

与 Spring MVC 中的相同

您可以使用@CookieValue注解将 HTTP cookie 的值绑定到控制器中的方法参数。

以下示例显示了一个带有 cookie 的请求:

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

下面的代码示例演示如何获取 cookie 值:

@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
    //...
}
  • (1) 获取 Cookie 值。

如果目标方法参数类型不是String,则会自动应用类型转换。参见[mvc-ann-typeconversion]

@ModelAttribute

与 Spring MVC 中的相同

您可以在方法参数上使用@ModelAttribute注解来访问模型中的属性,或将其实例化(如果不存在)。 model 属性还覆盖了查询参数的值和名称与字段名称匹配的表单字段。这称为数据绑定,它使您不必处理解析和转换单个查询参数和表单字段的工作。下面的示例绑定Pet的实例:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
  • (1) 绑定Pet的实例。

上例中的Pet实例按以下方式解析:

  • 从模型(如果已通过Model添加)。

  • 从 HTTP 会话通过@SessionAttributes

  • 从默认构造函数的调用开始。

  • 从带有匹配查询参数或表单字段的参数的“主要构造函数”的调用开始。参数名称是通过 JavaBeans @ConstructorProperties或字节码中运行时保留的参数名称确定的。

获取模型属性实例后,将应用数据绑定。 WebExchangeDataBinder类将查询参数和表单字段的名称与目标Object上的字段名称匹配。在必要时应用类型转换后,将填充匹配字段。有关数据绑定(和验证)的更多信息,请参见Validation。有关自定义数据绑定的更多信息,请参见DataBinder

数据绑定可能会导致错误。默认情况下,引发WebExchangeBindException,但是,要检查 controller 方法中的此类错误,可以在@ModelAttribute的紧后面添加BindingResult参数,如以下示例所示:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}
  • (1) 添加BindingResult

您可以在数据绑定之后通过添加javax.validation.ValidComments 或 Spring 的@ValidatedComments 来自动应用验证(另请参见Bean validationSpring validation)。以下示例使用@Valid注解:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}
  • (1) 在模型属性参数上使用@Valid

与 Spring MVC 不同,Spring WebFlux 在模型 supports 中支持反应类型,例如Mono<Account>io.reactivex.Single<Account>。您可以声明带有或不带有 Reactive 类型包装器的@ModelAttribute参数,并将根据需要将其解析为实际值。但是,请注意,要使用BindingResult参数,您必须在@ModelAttribute参数之前声明它而不使用反应式类型包装器,如先前所示。另外,您可以通过反应式处理任何错误,如以下示例所示:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
    return petMono
        .flatMap(pet -> {
            // ...
        })
        .onErrorResume(ex -> {
            // ...
        });
}

请注意,使用@ModelAttribute是可选的,例如用于设置其属性。默认情况下,任何不是简单值类型(由BeanUtils#isSimpleProperty确定)且未被其他任何参数解析器解析的参数都将被视为已用@ModelAttributeComments。

@SessionAttributes

与 Spring MVC 中的相同

@SessionAttributes用于在两次请求之间的WebSession中存储模型属性。它是类型级别的 Comments,用于声明特定控制器使用的会话属性。这通常列出应透明地存储在会话中以供后续访问请求的模型属性名称或模型属性类型。

考虑以下示例:

@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
    // ...
}
  • (1) 使用@SessionAttributesComments。

在第一个请求中,将名称为pet的模型属性添加到模型后,该属性会自动提升为WebSession并保存在其中。它会一直保留在那里,直到另一个控制器方法使用SessionStatus方法参数来清除存储,如以下示例所示:

@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {

    // ...

    @PostMapping("/pets/{id}")
    public String handle(Pet pet, BindingResult errors, SessionStatus status) { (2)
        if (errors.hasErrors) {
            // ...
        }
            status.setComplete();
            // ...
        }
    }
}
  • (1) 使用@SessionAttributesComments。
  • (2) 使用SessionStatus变量。
@SessionAttribute

与 Spring MVC 中的相同

如果您需要访问全局存在(例如,在控制器外部(例如,通过过滤器)Management)并且可能存在或可能不存在的预先存在的会话属性,则可以在方法参数上使用@SessionAttributeComments,作为以下示例显示:

@GetMapping("/")
public String handle(@SessionAttribute User user) { (1)
    // ...
}
  • (1) 使用@SessionAttribute

对于需要添加或删除会话属性的用例,请考虑将WebSession注入控制器方法。

要将模型属性临时存储在会话中作为控制器工作流的一部分,请考虑使用SessionAttributes,如@SessionAttributes中所述。

@RequestAttribute

与 Spring MVC 中的相同

@SessionAttribute相似,您可以使用@RequestAttribute注解来访问先前创建的预先存在的请求属性(例如WebFilter),如以下示例所示:

@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
    // ...
}
  • (1) 使用@RequestAttribute
Multipart Content

与 Spring MVC 中的相同

Multipart Data中所述,ServerWebExchange提供对 Multipart 内容的访问。在控制器中处理文件上传表单(例如,从浏览器)的最佳方法是通过将数据绑定到command object,如以下示例所示:

class MyForm {

    private String name;

    private MultipartFile file;

    // ...

}

@Controller
public class FileUploadController {

    @PostMapping("/form")
    public String handleFormUpload(MyForm form, BindingResult errors) {
        // ...
    }

}

您还可以在 RESTful 服务方案中从非浏览器 Client 端提交 Multipart 请求。以下示例将文件与 JSON 一起使用:

POST /someUrl
Content-Type: multipart/mixed

--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit

{
    "name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...

您可以使用@RequestPart来访问各个部分,如以下示例所示:

@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, (1)
        @RequestPart("file-data") FilePart file) { (2)
    // ...
}
  • (1) 使用@RequestPart获取元数据。
  • (2) 使用@RequestPart获取文件。

要反序列化原始 Component 的内容(例如,类似于@RequestBody的 JSON),可以声明一个具体的目标Object,而不是Part,如以下示例所示:

@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
    // ...
}
  • (1) 使用@RequestPart获取元数据。

您可以将@RequestPartjavax.validation.Valid或 Spring 的@Validated注解结合使用,这将导致应用标准 Bean 验证。默认情况下,验证错误会导致WebExchangeBindException,它变成 400(BAD_REQUEST)响应。或者,您可以通过ErrorsBindingResult参数在控制器内部本地处理验证错误,如以下示例所示:

@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata, (1)
        BindingResult result) { (2)
    // ...
}
  • (1) 使用@ValidComments。
  • (2) 使用BindingResult参数。

要以MultiValueMap的形式访问所有 Multipart 数据,可以使用@RequestBody,如以下示例所示:

@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
    // ...
}
  • (1) 使用@RequestBody

要以流方式 Sequences 访问 Multipart 数据,可以将@RequestBodyFlux<Part>结合使用,如以下示例所示:

@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) { (1)
    // ...
}
  • (1) 使用@RequestBody
@RequestBody

与 Spring MVC 中的相同

您可以使用@RequestBody注解将请求正文读取并通过HttpMessageReader反序列化为Object。以下示例使用@RequestBody参数:

@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
    // ...
}

与 Spring MVC 不同,在 WebFlux 中,@RequestBody方法参数支持响应类型以及完全无阻塞的读取和(Client 端到服务器)流。以下示例使用Mono

@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
    // ...
}

您可以使用WebFlux ConfigHTTP 消息编解码器选项来配置或自定义消息阅读器。

您可以将@RequestBodyjavax.validation.Valid或 Spring 的@Validated注解结合使用,这将导致应用标准 Bean 验证。默认情况下,验证错误会导致WebExchangeBindException,它变成 400(BAD_REQUEST)响应。或者,您可以通过ErrorsBindingResult参数在控制器内本地处理验证错误。以下示例使用BindingResult参数`:

@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, BindingResult result) {
    // ...
}
HttpEntity

与 Spring MVC 中的相同

HttpEntity与使用@RequestBody大致相同,但基于一个容器对象,该对象公开了请求 Headers 和正文。以下示例使用HttpEntity

@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
    // ...
}
@ResponseBody

与 Spring MVC 中的相同

您可以在方法上使用@ResponseBody注解,以使返回值通过HttpMessageWriter序列化到响应主体。以下示例显示了如何执行此操作:

@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
    // ...
}

@ResponseBody在类级别上也受支持,在这种情况下,它被所有控制器方法继承。这就是@RestController的效果,它不过是用@Controller@ResponseBody标记的元 Comments。

@ResponseBody支持反应式类型,这意味着您可以返回 Reactor 或 RxJava 类型,并将它们产生的异步值呈现给响应。有关更多详细信息,请参见StreamingJSON rendering

您可以将@ResponseBody方法与 JSON 序列化视图结合使用。有关详情,请参见Jackson JSON

您可以使用WebFlux ConfigHTTP 消息编解码器选项来配置或自定义消息编写。

ResponseEntity

与 Spring MVC 中的相同

ResponseEntity类似于@ResponseBody,但具有状态和标题。例如:

@GetMapping("/something")
public ResponseEntity<String> handle() {
    String body = ... ;
    String etag = ... ;
    return ResponseEntity.ok().eTag(etag).build(body);
}

WebFlux 支持使用单个值reactive type异步生成ResponseEntity,和/或为主体使用单值和多值反应类型。

Jackson JSON

Spring 提供了对 Jackson JSON 库的支持。

Jackson 序列化视图

与 Spring MVC 中的相同

Spring WebFlux 提供对Jackson 的序列化视图的内置支持,该支持仅渲染Object中所有字段的一部分。要将它与@ResponseBodyResponseEntity控制器方法一起使用,可以使用 Jackson 的@JsonView注解来激活序列化视图类,如以下示例所示:

@RestController
public class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView.class)
    public User getUser() {
        return new User("eric", "7!jd#h23");
    }
}

public class User {

    public interface WithoutPasswordView {};
    public interface WithPasswordView extends WithoutPasswordView {};

    private String username;
    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @JsonView(WithoutPasswordView.class)
    public String getUsername() {
        return this.username;
    }

    @JsonView(WithPasswordView.class)
    public String getPassword() {
        return this.password;
    }
}

Note

@JsonView允许一组视图类,但每个控制器方法只能指定一个。如果需要激活多个视图,请使用复合界面。

1.4.4. Model

与 Spring MVC 中的相同

您可以使用@ModelAttribute注解:

  • method argument in @RequestMapping方法上,可以从模型创建或访问对象,并通过WebDataBinder将其绑定到请求。

  • 作为@Controller@ControllerAdvice类中的方法级 Comments,有助于在任何@RequestMapping方法调用之前初始化模型。

  • @RequestMapping方法上将其返回值标记为模型属性。

本节讨论@ModelAttribute方法,或前面列表中的第二项。控制器可以具有任意数量的@ModelAttribute方法。所有此类方法均在同一控制器中的@RequestMapping方法之前调用。 @ModelAttribute方法也可以通过@ControllerAdvice在控制器之间共享。有关更多详细信息,请参见Controller Advice部分。

@ModelAttribute方法具有灵活的方法签名。它们支持许多与@RequestMapping方法相同的参数(@ModelAttribute本身以及与请求主体相关的任何东西除外)。

下面的示例使用@ModelAttribute方法:

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountRepository.findAccount(number));
    // add more ...
}

以下示例仅添加一个属性:

@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountRepository.findAccount(number);
}

Note

如果未明确指定名称,则根据类型选择默认名称,如Conventions的 javadoc 中所述。您始终可以使用重载的addAttribute方法或通过@ModelAttribute上的 name 属性(用于返回值)来分配显式名称。

与 Spring MVC 不同,Spring WebFlux 在模型中显式支持响应类型(例如Mono<Account>io.reactivex.Single<Account>)。可以在@RequestMapping调用时将此类异步模型属性透明地解析(并更新模型)为其实际值,前提是声明了不带包装的@ModelAttribute参数,如以下示例所示:

@ModelAttribute
public void addAccount(@RequestParam String number) {
    Mono<Account> accountMono = accountRepository.findAccount(number);
    model.addAttribute("account", accountMono);
}

@PostMapping("/accounts")
public String handle(@ModelAttribute Account account, BindingResult errors) {
    // ...
}

此外,任何具有 Reactive 类型包装器的模型属性都将在视图渲染之前解析为其实际值(并更新了模型)。

您也可以将@ModelAttribute用作@RequestMapping方法的方法级 Comments,在这种情况下@RequestMapping方法的返回值将解释为模型属性。通常不需要这样做,因为这是 HTML 控制器中的默认行为,除非返回值是String,否则它将被解释为视图名称。 @ModelAttribute还可以帮助自定义模型属性名称,如以下示例所示:

@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
    // ...
    return account;
}

1.4.5. DataBinder

与 Spring MVC 中的相同

@Controller@ControllerAdvice类可以具有@InitBinder方法,以初始化WebDataBinder的实例。这些依次用于:

  • 将请求参数(即表单数据或查询)绑定到模型对象。

  • 将基于String的请求值(例如请求参数,路径变量,Headers,Cookie 等)转换为控制器方法参数的目标类型。

  • 呈现 HTML 表单时,将模型对象的值格式化为String值。

@InitBinder个方法可以注册特定于控制器的java.bean.PropertyEditor或 Spring ConverterFormatter组件。此外,您可以使用WebFlux Java 配置在全局共享的FormattingConversionService中注册ConverterFormatter类型。

@InitBinder方法支持与@RequestMapping方法相同的许多参数,但@ModelAttribute(命令对象)参数除外。通常,它们使用WebDataBinder参数声明(用于注册)和void返回值。以下示例使用@InitBinder注解:

@Controller
public class FormController {

    @InitBinder (1)
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }

    // ...
}
  • (1) 使用@InitBinderComments。

或者,当通过共享的FormattingConversionService使用基于Formatter的设置时,可以重新使用相同的方法并注册特定于控制器的Formatter实例,如以下示例所示:

@Controller
public class FormController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); (1)
    }

    // ...
}
  • (1) 添加自定义格式程序(在本例中为DateFormatter)。

1.4.6. Management 异常

与 Spring MVC 中的相同

@Controller@ControllerAdvice类可以具有@ExceptionHandler个方法来处理控制器方法中的异常。下面的示例包括这样的处理程序方法:

@Controller
public class SimpleController {

    // ...

    @ExceptionHandler (1)
    public ResponseEntity<String> handle(IOException ex) {
        // ...
    }
}
  • (1) 声明@ExceptionHandler

该异常可以与正在传播的顶级异常(即直接抛出IOException)匹配,也可以与顶级包装程序异常中的直接原因匹配(例如,将IOException包裹在IllegalStateException内)。

对于匹配的异常类型,最好将目标异常声明为方法参数,如前面的示例所示。或者,Comments 声明可以缩小异常类型以使其匹配。我们通常建议在参数签名中尽可能具体,并在优先级为@ControllerAdvice的情况下以相应的 Sequences 声明您的主根异常 Map。有关详情,请参见MVC 部分

Note

WebFlux 中的@ExceptionHandler方法与@RequestMapping方法支持相同的方法参数和返回值,但与请求正文和@ModelAttribute相关的方法参数除外。

HandlerAdapter for @RequestMapping方法提供了对 Spring WebFlux 中@ExceptionHandler方法的支持。有关更多详细信息,请参见DispatcherHandler

REST API 异常

与 Spring MVC 中的相同

REST 服务的常见要求是在响应正文中包含错误详细信息。 Spring 框架不会自动这样做,因为响应主体中错误详细信息的表示是特定于应用程序的。但是,@RestController可以使用具有ResponseEntity返回值的@ExceptionHandler方法来设置响应的状态和主体。也可以在@ControllerAdvice类中声明此类方法以将其全局应用。

Note

请注意,Spring WebFlux 与 Spring MVC ResponseEntityExceptionHandler不具有等效项,因为 WebFlux 仅引发ResponseStatusException(或其子类),并且不需要将其转换为 HTTP 状态代码。

1.4.7. 控制器建议

与 Spring MVC 中的相同

通常,@ExceptionHandler@InitBinder@ModelAttribute方法适用于声明它们的@Controller类(或类层次结构)。如果希望此类方法更全局地应用(跨控制器),则可以在标有@ControllerAdvice@RestControllerAdvice的类中声明它们。

@ControllerAdvice标有@Component,这意味着可以通过component scanning将此类注册为 Spring Bean。 @RestControllerAdvice也是标有@ControllerAdvice@ResponseBody的元 Comments,从本质上讲,这意味着@ExceptionHandler方法通过消息转换(与视图分辨率或模板渲染)呈现给响应主体。

启动时,@RequestMapping@ExceptionHandler方法的基础结构类将检测@ControllerAdvice类型的 Spring bean,并在运行时应用其方法。全局@ExceptionHandler方法(来自@ControllerAdvice)在 本地方法(来自@Controller)之后 应用。相比之下,全局@ModelAttribute@InitBinder方法在**本地方法之前被应用。

默认情况下,@ControllerAdvice方法适用于每个请求(即所有控制器),但是您可以通过 Comments 中的属性将其缩小到控制器的子集,如以下示例所示:

// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

前面的 selectors 会在运行时进行评估,如果广泛使用它们,可能会对性能产生负面影响。有关更多详细信息,请参见@ControllerAdvice javadoc。

1.5. 功能端点

Spring WebFlux 包含 WebFlux.fn,这是一个轻量级的函数编程模型,其中的函数用于路由和处理请求,而契约则是为不变性而设计的。它是基于 Comments 的编程模型的替代方案,但可以在相同的Reactive Core基础上运行。

1.5.1. Overview

在 WebFlux.fn 中,HTTP 请求使用HandlerFunction处理:该函数接受ServerRequest并返回延迟的ServerResponse(即Mono<ServerResponse>)。作为请求对象的请求都具有不可变的协定,这些协定为 JDK 8 提供了对 HTTP 请求和响应的友好访问。 HandlerFunction等效于基于 Comments 的编程模型中@RequestMapping方法的主体。

传入的请求会通过RouterFunction路由到处理函数,该函数接受ServerRequest并返回延迟的HandlerFunction(即Mono<HandlerFunction>)。当 Router 功能匹配时,返回处理程序功能。否则为空 Mono。 RouterFunction等效于@RequestMappingComments,但主要区别在于 Router 功能不仅提供数据,还提供行为。

RouterFunctions.route()提供了一个 Router 构建器,可简化 Router 的创建过程,如以下示例所示:

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople)
    .POST("/person", handler::createPerson)
    .build();

public class PersonHandler {

    // ...

    public Mono<ServerResponse> listPeople(ServerRequest request) {
        // ...
    }

    public Mono<ServerResponse> createPerson(ServerRequest request) {
        // ...
    }

    public Mono<ServerResponse> getPerson(ServerRequest request) {
        // ...
    }
}

运行RouterFunction的一种方法是将其转换为HttpHandler并通过内置的server adapters安装:

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

大多数应用程序都可以通过 WebFlux Java 配置运行,请参阅运行服务器

1.5.2. HandlerFunction

ServerRequestServerResponse是不可变的接口,它们提供 JDK 8 友好的 HTTP 请求和响应访问。请求和响应都对体流提供Reactive Streams背压。请求主体用 Reactor FluxMono表示。响应主体由任何响应流Publisher表示,包括FluxMono。有关更多信息,请参见Reactive Libraries

ServerRequest

ServerRequest提供对 HTTP 方法,URI,Headers 和查询参数的访问,而通过body方法提供对正文的访问。

以下示例将请求正文提取到Mono<String>

Mono<String> string = request.bodyToMono(String.class);

以下示例将主体提取到Flux<Person>,其中Person对象是从某种序列化形式(例如 JSON 或 XML)解码的:

Flux<Person> people = request.bodyToFlux(Person.class);

前面的示例是使用更通用的ServerRequest.body(BodyExtractor)的快捷方式,该ServerRequest.body(BodyExtractor)接受BodyExtractor功能策略界面。Util 类BodyExtractors提供对许多实例的访问。例如,前面的示例也可以编写如下:

Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));

下面的示例显示如何访问表单数据:

Mono<MultiValueMap<String, String> map = request.body(BodyExtractors.toFormData());

以下示例显示了如何以 Map 的形式访问 Multipart 数据:

Mono<MultiValueMap<String, Part> map = request.body(BodyExtractors.toMultipartData());

下面的示例演示如何以流方式一次访问多个部分:

Flux<Part> parts = request.body(BodyExtractos.toParts());
ServerResponse

ServerResponse提供对 HTTP 响应的访问,并且由于它是不可变的,因此可以使用build方法来创建它。您可以使用构建器来设置响应状态,添加响应标题或提供正文。以下示例使用 JSON 内容创建 200(确定)响应:

Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);

下面的示例演示如何构建带有LocationHeaders 且不包含主体的 201(已创建)响应:

URI location = ...
ServerResponse.created(location).build();
Handler Classes

我们可以将处理程序函数编写为 lambda,如以下示例所示:

HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().body(fromObject("Hello World"));

这很方便,但是在应用程序中我们需要多个功能,并且多个内联 lambda 可能会变得凌乱。因此,将相关的处理程序功能分组到一个处理程序类中很有用,该类在与基于 Comments 的应用程序中的作用与@Controller类似。例如,以下类公开了一个响应式Person存储库:

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.ServerResponse.ok;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;

public class PersonHandler {

    private final PersonRepository repository;

    public PersonHandler(PersonRepository repository) {
        this.repository = repository;
    }

    public Mono<ServerResponse> listPeople(ServerRequest request) { (1)
        Flux<Person> people = repository.allPeople();
        return ok().contentType(APPLICATION_JSON).body(people, Person.class);
    }

    public Mono<ServerResponse> createPerson(ServerRequest request) { (2)
        Mono<Person> person = request.bodyToMono(Person.class);
        return ok().build(repository.savePerson(person));
    }

    public Mono<ServerResponse> getPerson(ServerRequest request) { (3)
        int personId = Integer.valueOf(request.pathVariable("id"));
        return repository.getPerson(personId)
            .flatMap(person -> ok().contentType(APPLICATION_JSON).body(fromObject(person)))
            .switchIfEmpty(ServerResponse.notFound().build());
    }
}
  • (1) listPeople是一个处理函数,它以 JSON 格式返回存储库中找到的所有Person对象。
  • (2) createPerson是一个处理函数,用于存储请求正文中包含的新Person。注意PersonRepository.savePerson(Person)返回Mono<Void>:当从请求中读取并存储此人时,空Mono发出完成 signal。因此,当收到完成 signal 时(即,保存Person时),我们使用build(Publisher<Void>)方法发送响应。
  • (3) getPerson是一个处理函数,它返回一个由id path 变量标识的人。我们从存储库中检索该Person,并创建一个 JSON 响应(如果找到)。如果找不到,我们使用switchIfEmpty(Mono<T>)返回 404 Not Found 响应。

1.5.3. RouterFunction

Router 功能用于将请求路由到相应的HandlerFunction。通常,您不是自己编写 Router 功能,而是使用RouterFunctions实用工具类上的方法创建一个。 RouterFunctions.route()(无参数)为您提供了流畅的生成器来创建 Router 功能,而RouterFunctions.route(RequestPredicate, HandlerFunction)提供了直接的方式来创建 Router。

通常,建议使用route()构建器,因为它为典型的 Map 方案提供了便捷的快捷方式,而无需发现静态导入。例如,Router 功能构建器提供了GET(String, HandlerFunction)方法来为 GET 请求创建 Map;和POST(String, HandlerFunction)(用于 POST)。

除了基于 HTTP 方法的 Map 外,路由构建器还提供了一种在 Map 到请求时引入其他谓词的方法。对于每个 HTTP 方法,都有一个以RequestPredicate作为参数的重载变体,但是可以表示其他约束。

Predicates

您可以编写自己的RequestPredicate,但是RequestPredicatesUtil 类根据请求路径,HTTP 方法,Content Type 等提供常用的实现。以下示例使用请求谓词基于AcceptHeaders 创建约束:

RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
        request -> Response.ok().body(fromObject("Hello World")));

您可以使用以下方法将多个请求谓词组合在一起:

  • RequestPredicate.and(RequestPredicate) —两者都必须匹配。

  • RequestPredicate.or(RequestPredicate) —都可以匹配。

RequestPredicates中的许多谓词组成。例如,RequestPredicates.GET(String)RequestPredicates.method(HttpMethod)RequestPredicates.path(String)组成。上面显示的示例还使用了两个请求谓词,因为构建器在内部使用RequestPredicates.GET并将其与accept谓词组合在一起。

Routes

Router 功能按 Sequences 评估:如果第一个路由不匹配,则评估第二个路由,依此类推。因此,在通用路由之前声明更具体的路由是有意义的。请注意,此行为不同于基于 Comments 的编程模型,在该模型中,将自动选择“最特定”的控制器方法。

使用 Router 功能生成器时,所有定义的路由都组成一个RouterFunction,从RouterFunction返回。还有其他方法可以将多个 Router 功能组合在一起:

  • add(RouterFunction)RouterFunctions.route()构建器上

  • RouterFunction.and(RouterFunction)

  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) —嵌套RouterFunctions.route()RouterFunction.and()的快捷方式。

以下示例显示了四种 Route 的组成:

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> otherRoute = ...

RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
    .POST("/person", handler::createPerson) (3)
    .add(otherRoute) (4)
    .build();
  • (1) 具有与 JSON 匹配的AcceptHeaders 的GET /person/{id}被路由到PersonHandler.getPerson
  • (2) 具有与 JSON 匹配的AcceptHeaders 的GET /person被路由到PersonHandler.listPeople
  • (3) POST /person没有其他谓词被 Map 到PersonHandler.createPerson,并且
  • (4) otherRoute是在其他地方创建并添加到所构建路由的 Router 功能。
Nested Routes

一组 Router 功能通常具有共享谓词,例如共享路径。在上面的示例中,共享谓词将是与其中三个路由使用的/person匹配的路径谓词。使用 Comments 时,您可以通过使用 Map 到/person的类型级别@RequestMappingComments 来删除此重复项。在 WebFlux.fn 中,可以通过 Router 功能构建器上的path方法共享路径谓词。例如,可以通过以下方式使用嵌套路由来改进上面示例的最后几行:

RouterFunction<ServerResponse> route = route()
    .path("/person", builder -> builder
        .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        .GET("", accept(APPLICATION_JSON), handler::listPeople)
        .POST("/person", handler::createPerson))
    .build();

请注意,path的第二个参数是使用 Router 构建器的使用者。

尽管基于路径的嵌套是最常见的,但是您可以通过使用构建器上的nest方法来嵌套在任何种类的谓词上。上面的代码仍然包含一些共享的Accept -header 谓词形式的重复项。通过结合使用nestaccept可以进一步改进:

RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET("", handler::listPeople))
        .POST("/person", handler::createPerson))
    .build();

1.5.4. 运行服务器

如何在 HTTP 服务器中运行 Router 功能?一个简单的选项是使用以下方法之一将 Router 功能转换为HttpHandler

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

然后,您可以按照HttpHandler中有关服务器特定的说明,将返回的HttpHandler与许多服务器适配器一起使用。

Spring Boot 也使用了一个更典型的选项,即通过WebFlux Config使用基于DispatcherHandler的设置来运行,该设置使用 Spring 配置来声明处理请求所需的组件。 WebFlux Java 配置声明以下基础结构组件以支持功能端点:

  • RouterFunctionMapping:在 Spring 配置中检测一个或多个RouterFunction<?> bean,通过RouterFunction.andOther组合它们,并将请求路由到生成的RouterFunction

  • HandlerFunctionAdapter:简单的适配器,它使DispatcherHandler调用 Map 到请求的HandlerFunction

  • ServerResponseResultHandler:通过调用ServerResponsewriteTo方法来处理HandlerFunction调用的结果。

前面的组件使功能端点适合DispatcherHandler请求处理生命周期,并且(如果有)声明的控制器也可以(可能)与带 Comments 的控制器并排运行。这也是 Spring Boot WebFlux 启动器启用功能端点的方式。

以下示例显示了 WebFlux Java 配置(有关如何运行它,请参见DispatcherHandler):

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Bean
    public RouterFunction<?> routerFunctionA() {
        // ...
    }

    @Bean
    public RouterFunction<?> routerFunctionB() {
        // ...
    }

    // ...

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        // configure message conversion...
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // configure CORS...
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // configure view resolution for HTML rendering...
    }
}

1.5.5. 过滤处理程序功能

您可以使用路由功能构建器上的beforeafterfilter方法来过滤处理程序函数。使用 Comments,可以通过使用@ControllerAdviceServletFilter或同时使用两者来实现类似的功能。该过滤器将应用于构建器构建的所有路由。这意味着在嵌套路由中定义的过滤器不适用于“顶级”路由。例如,考虑以下示例:

RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET("", handler::listPeople)
            .before(request -> ServerRequest.from(request) (1)
                .header("X-RequestHeader", "Value")
                .build()))
        .POST("/person", handler::createPerson))
    .after((request, response) -> logResponse(response)) (2)
    .build();
  • (1) 添加自定义请求 Headers 的before过滤器仅应用于两条 GET 路由。
  • (2) 记录响应的after过滤器应用于所有路由,包括嵌套路由。

Router 构建器上的filter方法采用HandlerFilterFunction:此函数采用ServerRequestHandlerFunction并返回ServerResponse。 handler 函数参数代表链中的下一个元素。这通常是路由到的处理程序,但是如果应用了多个,它也可以是另一个过滤器。

现在,我们可以在路由中添加一个简单的安全过滤器,假设我们有一个SecurityManager可以确定是否允许特定路径。以下示例显示了如何执行此操作:

SecurityManager securityManager = ...

RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET("", handler::listPeople))
        .POST("/person", handler::createPerson))
    .filter((request, next) -> {
        if (securityManager.allowAccessTo(request.path())) {
            return next.handle(request);
        }
        else {
            return ServerResponse.status(UNAUTHORIZED).build();
        }
    })
    .build();

前面的示例演示了调用next.handle(ServerRequest)是可选的。当允许访问时,我们仅允许执行处理函数。

除了在 Router 功能构建器上使用filter方法之外,还可以通过RouterFunction.filter(HandlerFilterFunction)将过滤器应用于现有 Router 功能。

Note

通过专用的CorsWebFilter为功能端点提供 CORS 支持。

1.6. URI 链接

与 Spring MVC 中的相同

本节描述了 Spring 框架中用于准备 URI 的各种选项。

1.6.1. UriComponents

Spring MVC 和 Spring WebFlux

UriComponentsBuilder帮助从具有变量的 URI 模板构建 URI,如以下示例所示:

UriComponents uriComponents = UriComponentsBuilder
        .fromUriString("http://example.com/hotels/{hotel}")  (1)
        .queryParam("q", "{q}")  (2)
        .encode() (3)
        .build(); (4)

URI uri = uriComponents.expand("Westin", "123").toUri();  (5)
  • (1) 带有 URI 模板的静态工厂方法。
  • (2) 添加或替换 URI 组件。
  • (3) 请求对 URI 模板和 URI 变量进行编码。
  • (4) 构建UriComponents
  • (5) 展开变量并获得URI

可以将前面的示例合并为一个链,并使用buildAndExpand进行缩短,如以下示例所示:

URI uri = UriComponentsBuilder
        .fromUriString("http://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("Westin", "123")
        .toUri();

您可以通过直接转到 URI(这意味着编码)来进一步缩短它,如以下示例所示:

URI uri = UriComponentsBuilder
        .fromUriString("http://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");

您可以使用完整的 URI 模板进一步缩短它,如以下示例所示:

URI uri = UriComponentsBuilder
        .fromUriString("http://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123");

1.6.2. UriBuilder

Spring MVC 和 Spring WebFlux

UriComponentsBuilder实现UriBuilder。您可以依次创建UriBuilderUriBuilderFactoryUriBuilderFactoryUriBuilder一起提供了一种可插入的机制,用于基于共享配置(例如基本 URL,编码首选项和其他详细信息)从 URI 模板构建 URI。

您可以使用UriBuilderFactory配置RestTemplateWebClient以自定义 URI 的准备。 DefaultUriBuilderFactoryUriBuilderFactory的默认实现,该实现在内部使用UriComponentsBuilder并公开共享的配置选项。

以下示例显示了如何配置RestTemplate

// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "http://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VARIABLES);

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

以下示例配置WebClient

// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "http://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VARIABLES);

WebClient client = WebClient.builder().uriBuilderFactory(factory).build();

此外,您也可以直接使用DefaultUriBuilderFactory。它类似于使用UriComponentsBuilder,但不是静态工厂方法,而是一个包含配置和首选项的实际实例,如以下示例所示:

String baseUrl = "http://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);

URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");

1.6.3. URI 编码

Spring MVC 和 Spring WebFlux

UriComponentsBuilder公开了两个级别的编码选项:

这两个选项都使用转义的八位字节替换非 ASCII 和非法字符。但是,第一个选项还会替换出现在 URI 变量中的具有保留含义的字符。

Tip

考虑“;”,它在路径上是合法的,但具有保留的含义。第一个选项代替“;” URI 变量中带有“%3B”,但 URI 模板中没有。相比之下,第二个选项永远不会替换“;”,因为它是路径中的合法字符。

在大多数情况下,第一个选项可能会产生预期的结果,因为它将 URI 变量视为要完全编码的不透明数据,而选项 2 仅在 URI 变量有意包含保留字符的情况下才有用。

以下示例使用第一个选项:

URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
            .queryParam("q", "{q}")
            .encode()
            .buildAndExpand("New York", "foo+bar")
            .toUri();

    // Result is "/hotel%20list/New%20York?q=foo%2Bbar"

您可以通过直接转到 URI(这意味着编码)来缩短前面的示例,如以下示例所示:

URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
            .queryParam("q", "{q}")
            .build("New York", "foo+bar")

您可以使用完整的 URI 模板进一步缩短它,如以下示例所示:

URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}?q={q}")
            .build("New York", "foo+bar")

WebClientRestTemplate通过UriBuilderFactory策略在内部扩展和编码 URI 模板。两者都可以使用自定义策略进行配置。如下例所示:

String baseUrl = "http://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();

DefaultUriBuilderFactory实现内部使用UriComponentsBuilder来扩展和编码 URI 模板。作为工厂,它提供了一个位置,可以根据以下一种编码模式来配置编码方法:

  • TEMPLATE_AND_VALUES:使用UriComponentsBuilder#encode()(对应于较早列表中的第一个选项)来预编码 URI 模板,并在扩展时严格编码 URI 变量。

  • VALUES_ONLY:不对 URI 模板进行编码,而是在将它们扩展到模板之前通过UriUtils#encodeUriUriVariables对 URI 变量进行严格编码。

  • URI_COMPONENTS:在扩展 URI 变量之后*,使用与较早列表中第二个选项相对应的UriComponents#encode()来编码 URI 组件值。

  • NONE:未应用编码。

出于历史原因和向后兼容性,RestTemplate设置为EncodingMode.URI_COMPONENTSWebClient依赖于DefaultUriBuilderFactory中的默认值,该默认值已从 5.0.x 中的EncodingMode.URI_COMPONENTS更改为 5.1 中的EncodingMode.TEMPLATE_AND_VALUES

1.7. CORS

与 Spring MVC 中的相同

Spring WebFlux 使您可以处理 CORS(跨源资源共享)。本节介绍如何执行此操作。

1.7.1. Introduction

与 Spring MVC 中的相同

出于安全原因,浏览器禁止 AJAX 调用当前来源以外的资源。例如,您可以将您的银行帐户放在一个标签中,将 evil.com 放在另一个标签中。来自 evil.com 的脚本不能使用您的凭据向您的银行 API 发出 AJAX 请求。例如,从您的帐户中提取资金!

跨域资源共享(CORS)是由most browsers实现的W3C specification,它使您可以指定授权哪种类型的跨域请求,而不是使用基于 IFRAME 或 JSONP 的安全性较低且功能较弱的变通办法。

1.7.2. Processing

与 Spring MVC 中的相同

CORS 规范区分飞行前,简单和实际要求。要了解 CORS 的工作原理,您可以阅读this article,或者阅读规范以获得更多详细信息。

Spring WebFlux HandlerMapping实现为 CORS 提供内置支持。成功将请求 Map 到处理程序后,HandlerMapping检查给定请求和处理程序的 CORS 配置,并采取进一步的措施。飞行前请求直接处理,而简单和实际的 CORS 请求被拦截,验证并设置了所需的 CORS 响应 Headers。

为了启用跨域请求(即OriginHeaders 存在并且与请求的主机不同),您需要具有一些显式声明的 CORS 配置。如果找不到匹配的 CORS 配置,则飞行前请求将被拒绝。没有将 CORSHeaders 添加到简单和实际 CORS 请求的响应中,因此,浏览器拒绝了它们。

每个HandlerMapping可以分别是configured,并具有基于 URL 模式的CorsConfigurationMap。在大多数情况下,应用程序使用 WebFlux Java 配置声明此类 Map,从而导致将单个全局 Map 传递给所有HadlerMappping实现。

您可以将HandlerMapping级别的全局 CORS 配置与更细粒度的处理程序级别的 CORS 配置结合使用。例如,带 Comments 的控制器可以使用类或方法级别的@CrossOriginComments(其他处理程序可以实现CorsConfigurationSource)。

整体配置和局部配置的组合规则通常是相加的,例如,所有全局和所有本地来源。对于只能接受单个值的那些属性(例如allowCredentialsmaxAge),局部变量将覆盖全局值。有关更多详细信息,请参见CorsConfiguration#combine(CorsConfiguration)

Tip

要从源中了解更多信息或进行高级自定义,请参阅:

  • CorsConfiguration

  • CorsProcessorDefaultCorsProcessor

  • AbstractHandlerMapping

1.7.3. @CrossOrigin

与 Spring MVC 中的相同

@CrossOriginComments 启用带 Comments 的控制器方法上的跨域请求,如以下示例所示:

@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}

默认情况下,@CrossOrigin允许:

  • All origins.

  • All headers.

  • 控制器方法 Map 到的所有 HTTP 方法。

默认情况下未启用allowedCredentials,因为它构建了一个信任级别,该级别公开了敏感的用户特定信息(例如 cookie 和 CSRF 令牌),仅在适当的地方使用。

maxAge设置为 30 分钟。

@CrossOrigin在类级别也受支持,并且被所有方法继承。下面的示例指定一个特定的域并将maxAge设置为一个小时:

@CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}

您可以在类和方法级别上都使用@CrossOrigin,如以下示例所示:

@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin("http://domain2.com") (2)
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}
  • (1) 在类使用@CrossOrigin
  • (2) 在方法级别使用@CrossOrigin

1.7.4. 全局配置

与 Spring MVC 中的相同

除了细粒度的控制器方法级配置之外,您可能还想定义一些全局 CORS 配置。您可以在任何HandlerMapping上分别设置基于 URL 的CorsConfigurationMap。但是,大多数应用程序都使用 WebFlux Java 配置来执行此操作。

默认情况下,全局配置启用以下功能:

  • All origins.

  • All headers.

  • GETHEADPOST方法。

默认情况下未启用allowedCredentials,因为它构建了一个信任级别,该级别公开了敏感的用户特定信息(例如 cookie 和 CSRF 令牌),仅在适当的地方使用。

maxAge设置为 30 分钟。

要在 WebFlux Java 配置中启用 CORS,可以使用CorsRegistry回调,如以下示例所示:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping("/api/**")
            .allowedOrigins("http://domain2.com")
            .allowedMethods("PUT", "DELETE")
            .allowedHeaders("header1", "header2", "header3")
            .exposedHeaders("header1", "header2")
            .allowCredentials(true).maxAge(3600);

        // Add more mappings...
    }
}

1.7.5. CORS WebFilter

与 Spring MVC 中的相同

您可以通过内置的CorsWebFilter来应用 CORS 支持,这很适合functional endpoints

要配置过滤器,可以声明一个CorsWebFilter bean 并将CorsConfigurationSource传递给其构造函数,如以下示例所示:

@Bean
CorsWebFilter corsFilter() {

    CorsConfiguration config = new CorsConfiguration();

    // Possibly...
    // config.applyPermitDefaultValues()

    config.setAllowCredentials(true);
    config.addAllowedOrigin("http://domain1.com");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);

    return new CorsWebFilter(source);
}

1.8. 网络安全

与 Spring MVC 中的相同

Spring Security项目为保护 Web 应用程序免受恶意攻击提供支持。请参阅 Spring Security 参考文档,包括:

1.9. 查看技术

与 Spring MVC 中的相同

Spring WebFlux 中视图技术的使用是可插入的。是否决定使用 Thymeleaf,FreeMarker 或其他某种视图技术,主要取决于配置更改。本章介绍与 Spring WebFlux 集成的视图技术。我们假设您已经熟悉View Resolution

1.9.1. Thymeleaf

与 Spring MVC 中的相同

Thymeleaf 是一种现代的服务器端 Java 模板引擎,它强调可以通过双击在浏览器中预览的自然 HTML 模板,这对于独立处理 UI 模板(例如,由设计人员)而无需使用非常有用。正在运行的服务器。 Thymeleaf 提供了广泛的功能集,并且正在积极地开发和维护。有关更完整的介绍,请参见Thymeleaf项目主页。

Thymeleaf 与 Spring WebFlux 的集成由 Thymeleaf 项目 Management。该配置涉及一些 Bean 声明,例如SpringResourceTemplateResolverSpringWebFluxTemplateEngineThymeleafReactiveViewResolver。有关更多详细信息,请参阅Thymeleaf+Spring和 WebFlux 集成announcement

1.9.2. FreeMarker

与 Spring MVC 中的相同

Apache FreeMarker是一个模板引擎,用于生成从 HTML 到电子邮件等的任何类型的文本输出。 Spring 框架具有内置的集成,可以将 Spring WebFlux 与 FreeMarker 模板一起使用。

View Configuration

与 Spring MVC 中的相同

以下示例显示了如何将 FreeMarker 配置为一种视图技术:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freemarker();
    }

    // Configure FreeMarker...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates");
        return configurer;
    }
}

您的模板需要存储在FreeMarkerConfigurer指定的目录中,如上例所示。给定上述配置,如果您的控制器返回视图名称welcome,则解析器将查找classpath:/templates/freemarker/welcome.ftl模板。

FreeMarker Configuration

与 Spring MVC 中的相同

您可以通过在FreeMarkerConfigurer bean 上设置适当的 bean 属性,将 FreeMarker 的“设置”和“ SharedVariables”直接传递给 FreeMarker Configuration对象(由 SpringManagement)。 freemarkerSettings属性需要一个java.util.Properties对象,而freemarkerVariables属性需要一个java.util.Map。以下示例显示了如何使用FreeMarkerConfigurer

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    // ...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        Map<String, Object> variables = new HashMap<>();
        variables.put("xml_escape", new XmlEscape());

        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates");
        configurer.setFreemarkerVariables(variables);
        return configurer;
    }
}

有关设置和变量应用于Configuration对象的详细信息,请参见 FreeMarker 文档。

1.9.3. 脚本视图

与 Spring MVC 中的相同

Spring 框架具有内置的集成,可以将 Spring WebFlux 与可以在JSR-223 Java 脚本引擎之上运行的任何模板库一起使用。下表显示了我们在不同脚本引擎上测试过的模板库:

Tip

集成任何其他脚本引擎的基本规则是,它必须实现ScriptEngineInvocable接口。

Requirements

与 Spring MVC 中的相同

您需要在 Classpath 上具有脚本引擎,其细节因脚本引擎而异:

  • Java 8 随附了Nashorn JavaScript 引擎。强烈建议使用可用的最新更新版本。

  • 应该添加JRuby作为 Ruby 支持的依赖项。

  • 应该添加Jython作为对 Python 支持的依赖。

  • 为了支持 Kotlin 脚本,应添加org.jetbrains.kotlin:kotlin-script-util依赖项和包含org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory行的META-INF/services/javax.script.ScriptEngineFactory文件。有关更多详细信息,请参见this example

您需要具有脚本模板库。一种针对 Javascript 的方法是通过WebJars

Script Templates

与 Spring MVC 中的相同

您可以声明一个ScriptTemplateConfigurer bean,以指定要使用的脚本引擎,要加载的脚本文件,要调用的函数以渲染模板等等。以下示例使用 Mustache 模板和 Nashorn JavaScript 引擎:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.scriptTemplate();
    }

    @Bean
    public ScriptTemplateConfigurer configurer() {
        ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
        configurer.setEngineName("nashorn");
        configurer.setScripts("mustache.js");
        configurer.setRenderObject("Mustache");
        configurer.setRenderFunction("render");
        return configurer;
    }
}

render函数使用以下参数调用:

  • String template:模板内容

  • Map model:视图模型

  • RenderingContext renderingContext:用于访问应用程序上下文,语言环境,模板加载器和 URL 的RenderingContext(自 5.0 开始)

Mustache.render()与该签名本地兼容,因此您可以直接调用它。

如果您的模板技术需要一些自定义,则可以提供一个实现自定义渲染功能的脚本。例如,Handlerbars需要在使用模板之前先对其进行编译,并且需要polyfill才能模拟服务器端脚本引擎中不可用的某些浏览器功能。下面的示例演示如何设置自定义渲染功能:

@Configuration
@EnableWebMvc
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.scriptTemplate();
    }

    @Bean
    public ScriptTemplateConfigurer configurer() {
        ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
        configurer.setEngineName("nashorn");
        configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
        configurer.setRenderFunction("render");
        configurer.setSharedEngine(false);
        return configurer;
    }
}

Note

当使用非线程安全脚本引擎和非并发设计的模板库(例如在 Nashorn 上运行的 Handlebars 或 React)时,需要将sharedEngine属性设置为false。在这种情况下,由于this bug,因此需要 Java 8u60 或更高版本。

polyfill.js仅定义 Handlebars 正常运行所需的window对象,如以下代码片段所示:

var window = {};

这个基本的render.js实现在使用模板之前先对其进行编译。生产就绪的实现还应该存储和重用缓存的模板或预编译的模板。这可以在脚本端以及您需要的任何自定义(例如,Management 模板引擎配置)上完成。以下示例显示了如何编译模板:

function render(template, model) {
    var compiledTemplate = Handlebars.compile(template);
    return compiledTemplate(model);
}

查看 Spring Framework 单元测试Javaresources,以获取更多配置示例。

1.9.4. JSON 和 XML

与 Spring MVC 中的相同

出于Content Negotiation的目的,根据 Client 端请求的 Content Type,能够在使用 HTML 模板呈现模型或使用其他格式(例如 JSON 或 XML)呈现模型之间进行切换非常有用。为此,Spring WebFlux 提供了HttpMessageWriterView,您可以使用HttpMessageWriterViewspring-web插入任何可用的Codecs,例如Jackson2JsonEncoderJackson2SmileEncoderJaxb2XmlEncoder

与其他视图技术不同,HttpMessageWriterView不需要ViewResolver,而是将configured作为默认视图。您可以配置一个或多个此类默认视图,并包装不同的HttpMessageWriter实例或Encoder实例。在运行时使用与请求的 Content Type 匹配的内容。

在大多数情况下,模型包含多个属性。要确定要序列化的对象,可以使用要渲染的模型属性的名称配置HttpMessageWriterView。如果模型仅包含一个属性,则使用该属性。

1.10. HTTP 缓存

与 Spring MVC 中的相同

HTTP 缓存可以显着提高 Web 应用程序的性能。 HTTP 缓存围绕Cache-Control响应 Headers 和后续的条件请求 Headers(例如Last-ModifiedETag)展开。 Cache-Control建议私有(例如浏览器)和公共(例如代理)缓存如何缓存和重复使用响应。 ETagHeaders 用于发出条件请求,如果内容未更改,则可能导致没有主体的 304(NOT_MODIFIED)。 ETag可以看作是Last-ModifiedHeaders 的更复杂的后继者。

本节描述了 Spring WebFlux 中可用的 HTTP 缓存相关选项。

1.10.1. CacheControl

与 Spring MVC 中的相同

CacheControl支持配置与Cache-ControlHeaders 相关的设置,并且在许多地方都作为参数接受:

RFC 7234描述了Cache-Control响应 Headers 的所有可能的指令,而CacheControl类型采用面向用例的方法,该方法着重于常见方案,如以下示例所示:

// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);

// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();

// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();

1.10.2. Controllers

与 Spring MVC 中的相同

控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为需要先计算资源的lastModifiedETag值,然后才能将其与条件请求 Headers 进行比较。控制器可以将ETagCache-Control设置添加到ResponseEntity,如以下示例所示:

@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {

    Book book = findBook(id);
    String version = book.getVersion();

    return ResponseEntity
            .ok()
            .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
            .eTag(version) // lastModified is also available
            .body(book);
}

如果与条件请求 Headers 的比较表明内容未更改,则前面的示例发送带有空主体的 304(NOT_MODIFIED)响应。否则,ETagCache-ControlHeaders 将添加到响应中。

您还可以在控制器中针对条件请求 Headers 进行检查,如以下示例所示:

@RequestMapping
public String myHandleMethod(ServerWebExchange exchange, Model model) {

    long eTag = ... (1)

    if (exchange.checkNotModified(eTag)) {
        return null; (2)
    }

    model.addAttribute(...); (3)
    return "myViewName";
}
  • (1) 特定于应用程序的计算。
  • (2) 响应已设置为 304(NOT_MODIFIED)。无需进一步处理。
  • (3) continue 进行请求处理。

可以使用三种变体来检查针对eTag值和lastModified值或两者的条件请求。对于有条件的GETHEAD请求,可以将响应设置为 304(NOT_MODIFIED)。对于条件POSTPUTDELETE,您可以改为将响应设置为 409(PRECONDITION_FAILED)以防止并发修改。

1.10.3. 静态资源

与 Spring MVC 中的相同

您应该为静态资源提供Cache-Control和条件响应 Headers,以实现最佳性能。请参阅有关配置Static Resources的部分。

1.11. WebFlux 配置

与 Spring MVC 中的相同

WebFlux Java 配置声明使用带 Comments 的控制器或功能端点来声明处理请求所必需的组件,并且它提供了用于自定义配置的 API。这意味着您不需要了解 Java 配置创建的底层 bean。但是,如果您想了解它们,则可以在WebFluxConfigurationSupport中查看它们,或在特殊 bean 类中阅读有关它们的更多信息。

要获得配置 API 中没有的更高级的自定义设置,您可以通过高级配置模式获得对配置的完全控制。

1.11.1. 启用 WebFlux 配置

与 Spring MVC 中的相同

您可以在 Java 配置中使用@EnableWebFluxComments,如以下示例所示:

@Configuration
@EnableWebFlux
public class WebConfig {
}

前面的示例注册了许多 Spring WebFlux infrastructure beans,并适应了 classpath 上可用的依赖项(对于 JSON,XML 等)。

1.11.2. WebFlux 配置 API

与 Spring MVC 中的相同

在 Java 配置中,您可以实现WebFluxConfigurer接口,如以下示例所示:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    // Implement configuration methods...

}

1.11.3. 转换,格式化

与 Spring MVC 中的相同

默认情况下,将安装NumberDate类型的格式化程序,包括对@NumberFormat@DateTimeFormat注解的支持。如果 Classpath 中存在 Joda-Time,则还将安装对 Joda-Time 格式库的完全支持。

下面的示例演示如何注册自定义格式器和转换器:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // ...
    }

}

Note

有关何时使用FormatterRegistrar实现的更多信息,请参见FormatterRegistrar SPIFormattingConversionServiceFactoryBean

1.11.4. Validation

与 Spring MVC 中的相同

默认情况下,如果Bean Validation存在于 Classpath(例如,Hibernate Validator)上,则LocalValidatorFactoryBean被注册为全局validator,以便与@Controller方法参数上的@ValidValidated一起使用。

在 Java 配置中,您可以自定义全局Validator实例,如以下示例所示:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public Validator getValidator(); {
        // ...
    }

}

请注意,您还可以在本地注册Validator实现,如以下示例所示:

@Controller
public class MyController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(new FooValidator());
    }

}

Tip

如果需要在某处注入LocalValidatorFactoryBean,请创建一个 bean 并用@Primary进行标记,以避免与 MVC 配置中声明的那个冲突。

1.11.5. Content Type 解析器

与 Spring MVC 中的相同

您可以配置 Spring WebFlux 如何根据请求确定@Controller实例的请求媒体类型。默认情况下,仅选中AcceptHeaders,但您也可以启用基于查询参数的策略。

以下示例显示如何自定义请求的 Content Type 解析:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
        // ...
    }
}

1.11.6. HTTP 消息编解码器

与 Spring MVC 中的相同

以下示例显示如何自定义如何读取和写入请求和响应正文:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        // ...
    }
}

ServerCodecConfigurer提供了一组默认的读取器和写入器。您可以使用它来添加更多读取器和写入器,自定义默认读取器或完全替换默认读取器。

对于 Jackson JSON 和 XML,请考虑使用Jackson2ObjectMapperBuilder,它使用以下属性自定义 Jackson 的默认属性:

如果在 Classpath 中检测到以下知名模块,它还将自动注册以下知名模块:

1.11.7. 查看解析器

与 Spring MVC 中的相同

以下示例显示如何配置视图分辨率:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // ...
    }
}

ViewResolverRegistry具有用于与 Spring Framework 集成的视图技术的快捷方式。以下示例使用 FreeMarker(这也需要配置基础 FreeMarker 视图技术):

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();
    }

    // Configure Freemarker...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates");
        return configurer;
    }
}

您还可以插入任何ViewResolver实现,如以下示例所示:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        ViewResolver resolver = ... ;
        registry.viewResolver(resolver);
    }
}

要支持Content Negotiation并通过视图分辨率(除 HTML 之外)呈现其他格式,您可以基于HttpMessageWriterView实现配置一个或多个默认视图,该实现接受spring-web中的任何可用Codecs。以下示例显示了如何执行此操作:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();

        Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
        registry.defaultViews(new HttpMessageWriterView(encoder));
    }

    // ...
}

有关与 Spring WebFlux 集成的视图技术的更多信息,请参见View Technologies

1.11.8. 静态资源

与 Spring MVC 中的相同

此选项提供了一种方便的方式来从基于Resource的位置列表中提供静态资源。

在下一个示例中,给定一个以/resources开头的请求,相对路径用于在 Classpath 上查找和提供与/static相关的静态资源。资源的有效期为一年,以确保最大程度地利用浏览器缓存并减少浏览器发出的 HTTP 请求。 Last-ModifiedHeaders 也会被评估,如果存在,则返回304状态码。以下列表显示了示例:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
            .addResourceLocations("/public", "classpath:/static/")
            .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
    }

}

资源处理程序还支持ResourceResolver个实现和ResourceTransformer个实现的链,可用于创建工具链以使用优化的资源。

您可以将VersionResourceResolver用于基于资源,固定应用程序版本或其他信息计算出的 MD5 哈希的版本化资源 URL。 ContentVersionStrategy(MD5 哈希)是一个不错的选择,但有一些值得注意的 exception(例如与模块加载器一起使用的 JavaScript 资源)。

以下示例显示了如何在 Java 配置中使用VersionResourceResolver

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
    }

}

您可以使用ResourceUrlProvider重写 URL 并应用完整的解析器和转换器链(例如,插入版本)。 WebFlux 配置提供ResourceUrlProvider,以便可以将其注入其他对象。

与 Spring MVC 不同,目前,在 WebFlux 中,由于没有视图技术可以利用解析器和转换器的无阻塞链,因此无法透明地重写静态资源 URL。当仅提供本地资源时,解决方法是直接使用ResourceUrlProvider(例如,通过自定义元素)并进行阻止。

请注意,当同时使用EncodedResourceResolver(例如,Gzip,Brotli 编码)和VersionedResourceResolver时,必须按该 Sequences 注册它们,以确保始终基于未编码文件可靠地计算基于内容的版本。

WebJars也受WebJarsResourceResolver支持,并且当org.webjars:webjars-locator存在于 Classpath 中时会自动注册。解析器可以重写 URL 以包括 jar 的版本,并且还可以与没有版本的传入 URL 匹配(例如/jquery/jquery.min.js/jquery/1.2.0/jquery.min.js)。

1.11.9. 路径匹配

与 Spring MVC 中的相同

您可以自定义与路径匹配有关的选项。有关各个选项的详细信息,请参见PathMatchConfigurer javadoc。以下示例显示了如何使用PathMatchConfigurer

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer
            .setUseCaseSensitiveMatch(true)
            .setUseTrailingSlashMatch(false)
            .addPathPrefix("/api",
                    HandlerTypePredicate.forAnnotation(RestController.class));
    }

}

Tip

Spring WebFlux 依赖于名为RequestPath的请求路径的解析表示形式,以访问已解码的路径段值,并删除了分号内容(即路径或矩阵变量)。这意味着,与 Spring MVC 不同,您无需指示是否解码请求路径,也无需指示是否出于路径匹配目的而删除分号内容。

Spring WebFlux 也不支持后缀模式匹配,这与 Spring MVC 不同,在 Spring MVC 中,我们也recommend不再依赖它。

1.11.10. 高级配置模式

与 Spring MVC 中的相同

@EnableWebFlux导入DelegatingWebFluxConfiguration

  • 为 WebFlux 应用程序提供默认的 Spring 配置

  • 检测并委托WebFluxConfigurer个实现来自定义该配置。

对于高级模式,可以删除@EnableWebFlux并直接从DelegatingWebFluxConfiguration扩展,而不是实现WebFluxConfigurer,如以下示例所示:

@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {

    // ...

}

您可以将现有方法保留在WebConfig中,但是现在您还可以覆盖 Base Class 中的 bean 声明,并且在 Classpath 上仍然具有任意数量的其他WebMvcConfigurer实现。

1.12. HTTP/2

与 Spring MVC 中的相同

需要 Servlet 4 容器支持 HTTP/2,并且 Spring Framework 5 与 Servlet API 4 兼容。从编程模型的角度来看,应用程序不需要做任何特定的事情。但是,有一些与服务器配置有关的注意事项。有关更多详细信息,请参见HTTP/2 Wiki 页面

当前,Spring WebFlux 不支持 Netty 的 HTTP/2.也没有支持以编程方式将资源推送到 Client 端。

2. WebClient

Spring WebFlux 包括一个用于 HTTP 请求的 Reactive 非阻塞WebClient。Client 端具有功能性,Fluent 的 API,具有用于声明式组合的反应式类型,请参见Reactive Libraries。 WebFluxClient 端和服务器依靠相同的非阻塞codecs对请求和响应内容进行编码和解码。

在内部WebClient委托给 HTTPClient 端库。默认情况下,它使用Reactor Netty,内置了对 Jetty reactive HtpClient的支持,其他的可以通过ClientHttpConnector插入。

2.1. Configuration

创建WebClient的最简单方法是通过静态工厂方法之一:

  • WebClient.create()

  • WebClient.create(String baseUrl)

上面的方法使用具有默认设置的 Reactor Netty HttpClient,并期望io.projectreactor.netty:reactor-netty位于 Classpath 中。

您还可以将WebClient.builder()与其他选项一起使用:

  • uriBuilderFactory:自定义UriBuilderFactory用作基本 URL。

  • defaultHeader:每个请求的标题。

  • defaultCookie:每个请求的 cookie。

  • defaultRequestConsumer自定义每个请求。

  • filter:针对每个请求的 Client 端过滤器。

  • exchangeStrategies:HTTP 消息读取器/写入器定制。

  • clientConnector:HTTPClient 端库设置。

以下示例配置HTTP codecs

ExchangeStrategies strategies = ExchangeStrategies.builder()
            .codecs(configurer -> {
                // ...
            })
            .build();

    WebClient client = WebClient.builder()
            .exchangeStrategies(strategies)
            .build();

构建后,WebClient实例是不可变的。但是,您可以克隆它并构建修改后的副本,而不会影响原始实例,如以下示例所示:

WebClient client1 = WebClient.builder()
            .filter(filterA).filter(filterB).build();

    WebClient client2 = client1.mutate()
            .filter(filterC).filter(filterD).build();

    // client1 has filterA, filterB

    // client2 has filterA, filterB, filterC, filterD

2.1.1. 反应堆净值

要自定义 Reactor Netty 设置,只需提供一个预先配置的HttpClient

HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);

    WebClient webClient = WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();
Resources

默认情况下,HttpClient参与reactor.netty.http.HttpResources拥有的全局 Reactor Netty 资源,包括事件循环线程和连接池。这是推荐的模式,因为固定的共享资源是事件循环并发的首选。在这种模式下,全局资源将保持活动状态,直到进程退出。

如果服务器为该进程计时,则通常无需显式关闭。但是,如果服务器可以启动或停止进程内(例如,作为 WAR 部署的 Spring MVC 应用程序),则可以声明ReactorResourceFactoryglobalResources=true(默认值)类型的 Spring 托管 bean,以确保 Reactor Netty 全局 SpringApplicationContext关闭时,资源将关闭,如以下示例所示:

@Bean
    public ReactorResourceFactory reactorResourceFactory() {
        return new ReactorResourceFactory();
    }

您也可以选择不参与全局 Reactor Netty 资源。但是,在这种模式下,确保所有 Reactor NettyClient 端和服务器实例使用共享资源是您的重担,如以下示例所示:

@Bean
    public ReactorResourceFactory resourceFactory() {
        ReactorResourceFactory factory = new ReactorResourceFactory();
        factory.setGlobalResources(false); (1)
        return factory;
    }

    @Bean
    public WebClient webClient() {

        Function<HttpClient, HttpClient> mapper = client -> {
            // Further customizations...
        };

        ClientHttpConnector connector =
                new ReactorClientHttpConnector(resourceFactory(), mapper); (2)

        return WebClient.builder().clientConnector(connector).build(); (3)
    }
  • (1) 创建独立于全局资源的资源。
  • (2)ReactorClientHttpConnector构造函数与资源工厂一起使用。
  • (3) 将连接器插入WebClient.Builder
Timeouts

要配置连接超时:

import io.netty.channel.ChannelOption;

HttpClient httpClient = HttpClient.create()
        .tcpConfiguration(client ->
                client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000));

要配置读取和/或写入超时值:

import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;

HttpClient httpClient = HttpClient.create()
        .tcpConfiguration(client ->
                client.doOnConnected(conn -> conn
                        .addHandlerLast(new ReadTimeoutHandler(10))
                        .addHandlerLast(new WriteTimeoutHandler(10))));

2.1.2. Jetty

以下示例显示了如何自定义 Jetty HttpClient设置:

HttpClient httpClient = new HttpClient();
    httpClient.setCookieStore(...);
    ClientHttpConnector connector = new JettyClientHttpConnector(httpClient);

    WebClient webClient = WebClient.builder().clientConnector(connector).build();

默认情况下,HttpClient创建自己的资源(ExecutorByteBufferPoolScheduler),这些资源将保持活动状态,直到进程退出或调用stop()为止。

您可以在 JettyClient 端(和服务器)的多个实例之间共享资源,并pass 语句类型为JettyResourceFactory的 Spring 托管 bean 来确保在关闭 Spring ApplicationContext时关闭资源,如以下示例所示:

@Bean
    public JettyResourceFactory resourceFactory() {
        return new JettyResourceFactory();
    }

    @Bean
    public WebClient webClient() {

        Consumer<HttpClient> customizer = client -> {
            // Further customizations...
        };

        ClientHttpConnector connector =
                new JettyClientHttpConnector(resourceFactory(), customizer); (1)

        return WebClient.builder().clientConnector(connector).build(); (2)
    }
  • (1)JettyClientHttpConnector构造函数与资源工厂一起使用。
  • (2) 将连接器插入WebClient.Builder

2.2. retrieve()

retrieve()方法是获取响应正文并将其解码的最简单方法。以下示例显示了如何执行此操作:

WebClient client = WebClient.create("http://example.org");

    Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .bodyToMono(Person.class);

您还可以从响应中解码出一个对象流,如以下示例所示:

Flux<Quote> result = client.get()
            .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
            .retrieve()
            .bodyToFlux(Quote.class);

默认情况下,带有 4xx 或 5xx 状态代码的响应会导致WebClientResponseException或其 HTTP 状态特定的子类之一,例如WebClientResponseException.BadRequestWebClientResponseException.NotFound等。您还可以使用onStatus方法来自定义产生的异常,如以下示例所示:

Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .onStatus(HttpStatus::is4xxServerError, response -> ...)
            .onStatus(HttpStatus::is5xxServerError, response -> ...)
            .bodyToMono(Person.class);

使用onStatus时,如果期望响应包含内容,则onStatus回调应使用它。否则,内容将自动耗尽以确保释放资源。

2.3. exchange()

exchange()方法比retrieve方法提供更多的控制权。以下示例与retrieve()等效,但也提供对ClientResponse的访问:

Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .exchange()
            .flatMap(response -> response.bodyToMono(Person.class));

在此级别,您还可以创建完整的ResponseEntity

Mono<ResponseEntity<Person>> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .exchange()
            .flatMap(response -> response.toEntity(Person.class));

请注意(与retrieve()不同),对于exchange(),没有 4xx 和 5xx 响应的自动错误 signal。您必须检查状态码并决定如何进行。

Warning

使用exchange()时,必须始终使用ClientResponse的任何bodytoEntity方法,以确保释放资源并避免 HTTP 连接池的潜在问题。如果不需要响应内容,则可以使用bodyToMono(Void.class)。但是,如果响应中确实包含内容,则连接将关闭并且不会放回池中。

2.4. 请求正文

可以从Object编码请求主体,如以下示例所示:

Mono<Person> personMono = ... ;

    Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_JSON)
            .body(personMono, Person.class)
            .retrieve()
            .bodyToMono(Void.class);

您还可以对对象流进行编码,如以下示例所示:

Flux<Person> personFlux = ... ;

    Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_STREAM_JSON)
            .body(personFlux, Person.class)
            .retrieve()
            .bodyToMono(Void.class);

或者,如果您具有实际值,则可以使用syncBody快捷方式,如以下示例所示:

Person person = ... ;

    Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_JSON)
            .syncBody(person)
            .retrieve()
            .bodyToMono(Void.class);

2.4.1. 表格数据

要发送表单数据,您可以提供MultiValueMap<String, String>作为正文。请注意,内容会由FormHttpMessageWriter自动设置为application/x-www-form-urlencoded。以下示例显示了如何使用MultiValueMap<String, String>

MultiValueMap<String, String> formData = ... ;

    Mono<Void> result = client.post()
            .uri("/path", id)
            .syncBody(formData)
            .retrieve()
            .bodyToMono(Void.class);

您还可以使用BodyInserters在线提供表单数据,如以下示例所示:

import static org.springframework.web.reactive.function.BodyInserters.*;

    Mono<Void> result = client.post()
            .uri("/path", id)
            .body(fromFormData("k1", "v1").with("k2", "v2"))
            .retrieve()
            .bodyToMono(Void.class);

2.4.2. Multipart 数据

要发送 Multipart 数据,您需要提供一个MultiValueMap<String, ?>,其值要么是代表 Component 内容的Object实例,要么是代表 Component 内容和头的HttpEntity实例。 MultipartBodyBuilder提供了方便的 API 以准备 Multipart 请求。以下示例显示了如何创建MultiValueMap<String, ?>

MultipartBodyBuilder builder = new MultipartBodyBuilder();
    builder.part("fieldPart", "fieldValue");
    builder.part("filePart", new FileSystemResource("...logo.png"));
    builder.part("jsonPart", new Person("Jason"));

    MultiValueMap<String, HttpEntity<?>> parts = builder.build();

在大多数情况下,您不必为每个部分指定Content-Type。Content Type 是根据要序列化的HttpMessageWriter自动确定的,对于Resource则根据文件 extensions 自动确定。如有必要,您可以通过重载的生成器part方法之一显式提供MediaType供每个 Component 使用。

准备好MultiValueMap之后,将其传递给WebClient的最简单方法是通过syncBody方法,如以下示例所示:

MultipartBodyBuilder builder = ...;

    Mono<Void> result = client.post()
            .uri("/path", id)
            .syncBody(builder.build())
            .retrieve()
            .bodyToMono(Void.class);

如果MultiValueMap包含至少一个非String值,该值也可以表示常规表单数据(即application/x-www-form-urlencoded),则无需将Content-Type设置为multipart/form-data。使用MultipartBodyBuilder时,总是这样,以确保HttpEntity包装器。

作为MultipartBodyBuilder的替代方法,您还可以通过内置的BodyInserters提供内联样式的 Multipart 内容,如以下示例所示:

import static org.springframework.web.reactive.function.BodyInserters.*;

    Mono<Void> result = client.post()
            .uri("/path", id)
            .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
            .retrieve()
            .bodyToMono(Void.class);

2.5. Client 端过滤器

您可以通过WebClient.Builder注册 Client 端过滤器(ExchangeFilterFunction),以拦截和修改请求,如以下示例所示:

WebClient client = WebClient.builder()
        .filter((request, next) -> {

            ClientRequest filtered = ClientRequest.from(request)
                    .header("foo", "bar")
                    .build();

            return next.exchange(filtered);
        })
        .build();

这可以用于跨领域的关注,例如身份验证。以下示例使用过滤器通过静态工厂方法进行基本身份验证:

// static import of ExchangeFilterFunctions.basicAuthentication

WebClient client = WebClient.builder()
        .filter(basicAuthentication("user", "password"))
        .build();

过滤器全局应用于每个请求。要更改特定请求的过滤器行为,可以将请求属性添加到ClientRequest,然后链中的所有过滤器都可以访问该属性,如以下示例所示:

WebClient client = WebClient.builder()
        .filter((request, next) -> {
            Optional<Object> usr = request.attribute("myAttribute");
            // ...
        })
        .build();

client.get().uri("http://example.org/")
        .attribute("myAttribute", "...")
        .retrieve()
        .bodyToMono(Void.class);

    }

您也可以复制现有的WebClient,插入新的过滤器或删除已注册的过滤器。以下示例在索引 0 处插入一个基本身份验证过滤器:

// static import of ExchangeFilterFunctions.basicAuthentication

WebClient client = webClient.mutate()
        .filters(filterList -> {
            filterList.add(0, basicAuthentication("user", "password"));
        })
        .build();

2.6. Testing

要测试使用WebClient的代码,可以使用模拟 Web 服务器,例如OkHttp MockWebServer。要查看其用法示例,请查看 Spring Framework 测试套件中的WebClientIntegrationTests或 OkHttp 存储库中的static-server示例。

3. WebSockets

与 Servlet 堆栈中的相同

参考文档的此部分涵盖对反应堆 WebSocket 消息传递的支持。

3.1. WebSocket 介绍

WebSocket 协议RFC 6455提供了一种标准化方法,可以通过单个 TCP 连接在 Client 端和服务器之间构建全双工双向通信通道。它是与 HTTP 不同的 TCP 协议,但旨在通过端口 80 和 443 在 HTTP 上工作,并允许重复使用现有的防火墙规则。

WebSocket 交互始于一个 HTTP 请求,该请求使用 HTTP UpgradeHeaders 进行升级,或者在这种情况下切换到 WebSocket 协议。以下示例显示了这种交互:

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket (1)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
  • (1) UpgradeHeaders。
  • (2) 使用Upgrade连接。

具有 WebSocket 支持的服务器代替通常的 200 状态代码,返回类似于以下内容的输出:

HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
  • (1) 协议切换

成功握手后,HTTP 升级请求的基础 TCP 套接字将保持打开状态,Client 端和服务器均可 continue 发送和接收消息。

WebSockets 的工作原理的完整介绍超出了本文档的范围。请参阅 RFC 6455,HTML5 的 WebSocket 章节或 Web 上的许多简介和教程中的任何一个。

请注意,如果 WebSocket 服务器在 Web 服务器(例如 nginx)后面运行,则可能需要对其进行配置,以将 WebSocket 升级请求传递到 WebSocket 服务器。同样,如果应用程序在云环境中运行,请检查与 WebSocket 支持相关的云提供商的说明。

3.1.1. HTTP 与 WebSocket

尽管 WebSocket 设计为与 HTTP 兼容并以 HTTP 请求开头,但重要的是要了解这两个协议导致了截然不同的体系结构和应用程序编程模型。

在 HTTP 和 REST 中,应用程序被建模为许多 URL。为了与应用程序交互,Client 端访问那些 URL,即请求-响应样式。服务器根据 HTTP URL,方法和 Headers 将请求路由到适当的处理程序。

相比之下,在 WebSockets 中,初始连接通常只有一个 URL。随后,所有应用程序消息在同一 TCP 连接上流动。这指向了完全不同的异步,事件驱动的消息传递体系结构。

WebSocket 也是一种低级传输协议,与 HTTP 不同,它不对消息的内容规定任何语义。这意味着除非 Client 端和服务器就消息语义达成一致,否则就无法路由或处理消息。

WebSocketClient 端和服务器可以通过 HTTP 握手请求上的Sec-WebSocket-ProtocolHeaders 协商使用更高级别的消息传递协议(例如 STOMP)。在这种情况下,他们需要提出自己的约定。

3.1.2. 何时使用 WebSockets

WebSockets 可以使网页具有动态性和交互性。但是,在许多情况下,结合使用 Ajax 和 HTTP 流或长时间轮询可以提供一种简单有效的解决方案。

例如,新闻,邮件和社交订阅源需要动态更新,但是每隔几分钟这样做是完全可以的。另一方面,协作,游戏和金融应用程序需要更接近实时。

仅延迟并不是决定因素。如果消息量相对较少(例如,监视网络故障),则 HTTP 流或轮询可以提供有效的解决方案。低延迟,高频率和高音量的结合才是使用 WebSocket 的最佳案例。

还请记住,在 Internet 上,控件之外的限制性代理可能会阻止 WebSocket 交互,这可能是因为未将它们配置为传递UpgradeHeaders,或者是因为它们关闭了长期处于空闲状态的连接。这意味着与面向公众的应用程序相比,将 WebSocket 用于防火墙内部的应用程序是一个更直接的决定。

3.2. WebSocket API

与 Servlet 堆栈中的相同

Spring 框架提供了一个 WebSocket API,可用于编写处理 WebSocket 消息的 Client 端和服务器端应用程序。

3.2.1. Server

与 Servlet 堆栈中的相同

要创建 WebSocket 服务器,您可以先创建一个WebSocketHandler。以下示例显示了如何执行此操作:

import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;

public class MyWebSocketHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        // ...
    }
}

然后,您可以将其 Map 到 URL 并添加WebSocketHandlerAdapter,如以下示例所示:

@Configuration
static class WebConfig {

    @Bean
    public HandlerMapping handlerMapping() {
        Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/path", new MyWebSocketHandler());

        SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
        mapping.setUrlMap(map);
        mapping.setOrder(-1); // before annotated controllers
        return mapping;
    }

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}

3.2.2. WebSocketHandler

WebSocketHandlerhandle方法采用WebSocketSession并返回Mono<Void>来指示会话的应用程序处理何时完成。通过两个流处理会话,一个流用于入站消息,一个流用于出站消息。下表描述了两种处理流的方法:

WebSocketSession方法 Description
Flux<WebSocketMessage> receive() 提供对入站消息流的访问,并在关闭连接后完成。
Mono<Void> send(Publisher<WebSocketMessage>) 获取传出消息的源,编写消息,然后返回Mono<Void>,该源完成并写入后即完成。

WebSocketHandler必须将入站和出站流组成一个统一的流,并返回Mono<Void>以反映该流的完成。根据应用程序要求,统一流程在以下情况下完成:

  • 入站或出站消息流完成。

  • 入站流完成(即,连接已关闭),而出站流是无限的。

  • 在选定的位置,通过WebSocketSessionclose方法。

将入站和出站消息流组合在一起时,无需检查连接是否打开,因为“响应流”signal 会终止活动。入站流接收完成或错误 signal,而出站流接收取消 signal。

处理程序最基本的实现是处理入站流的实现。以下示例显示了这样的实现:

class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        return session.receive()            (1)
                .doOnNext(message -> {
                    // ...                  (2)
                })
                .concatMap(message -> {
                    // ...                  (3)
                })
                .then();                    (4)
    }
}
  • (1) 访问入站消息流。
  • (2) 对每条消息进行处理。
  • (3) 执行使用消息内容的嵌套异步操作。
  • (4) 返回一个Mono<Void>,它在接收完成时会完成。

Tip

对于嵌套的异步操作,您可能需要在使用池化数据缓冲区(例如 Netty)的基础服务器上调用message.retain()。否则,在您有机会读取数据之前,可能会释放数据缓冲区。有关更多背景信息,请参见数据缓冲区和编解码器

以下实现将入站和出站流组合在一起:

class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {

        Flux<WebSocketMessage> output = session.receive()               (1)
                .doOnNext(message -> {
                    // ...
                })
                .concatMap(message -> {
                    // ...
                })
                .map(value -> session.textMessage("Echo " + value));    (2)

        return session.send(output);                                    (3)
    }
}
  • (1) 处理入站消息流。
  • (2) 创建出站消息,从而产生组合流。
  • (3) 返回Mono<Void>,但我们 continue 接收时未完成。

入站和出站流可以是独立的,并且只能为了完成而加入,如以下示例所示:

class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {

        Mono<Void> input = session.receive()                                (1)
                .doOnNext(message -> {
                    // ...
                })
                .concatMap(message -> {
                    // ...
                })
                .then();

        Flux<String> source = ... ;
        Mono<Void> output = session.send(source.map(session::textMessage)); (2)

        return Mono.zip(input, output).then();                              (3)
    }
}
  • (1) 处理入站消息流。
  • (2) 发送传出邮件。
  • (3) 加入流并返回一个Mono<Void>,当任何一个流结束时,该Mono<Void>完成。

3.2.3. DataBuffer

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

在 Netty 上运行时,如果应用程序希望保留 Importing 数据缓冲区以确保它们不被释放,则必须使用DataBufferUtils.retain(dataBuffer),然后在使用缓冲区时使用DataBufferUtils.release(dataBuffer)

3.2.4. Handshake

与 Servlet 堆栈中的相同

WebSocketHandlerAdapter代表WebSocketService。默认情况下,它是HandshakeWebSocketService的实例,该实例对 WebSocket 请求执行基本检查,然后将RequestUpgradeStrategy用于所使用的服务器。当前,内置了对 Reactor Netty,Tomcat,Jetty 和 Undertow 的支持。

HandshakeWebSocketService公开了sessionAttributePredicate属性,该属性允许设置Predicate<String>以从WebSession提取属性并将其插入WebSocketSession的属性。

3.2.5. 服务器配置

与 Servlet 堆栈中的相同

每个服务器的RequestUpgradeStrategy公开了可用于基础 WebSocket 引擎的 WebSocket 相关配置选项。以下示例在 Tomcat 上运行时设置 WebSocket 选项:

@Configuration
static class WebConfig {

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter(webSocketService());
    }

    @Bean
    public WebSocketService webSocketService() {
        TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
        strategy.setMaxSessionIdleTimeout(0L);
        return new HandshakeWebSocketService(strategy);
    }
}

检查服务器的升级策略,以查看可用的选项。当前,只有 Tomcat 和 Jetty 公开了此类选项。

3.2.6. CORS

与 Servlet 堆栈中的相同

配置 CORS 并限制对 WebSocket 端点的访问的最简单方法是让WebSocketHandler实现CorsConfigurationSource并返回CorsConfiguraiton并带有允许的来源,Headers 和其他详细信息。如果您不能这样做,则还可以在SimpleUrlHandler上设置corsConfigurations属性,以通过 URL 模式指定 CORS 设置。如果同时指定了两者,则使用CorsConfiguration上的combine方法将它们组合在一起。

3.2.7. Client

Spring WebFlux 为 Reactor Netty,Tomcat,Jetty,Undertow 和标准 Java(即 JSR-356)的实现提供了WebSocketClient抽象。

Note

TomcatClient 端实际上是标准 JavaClient 端的扩展,在WebSocketSession处理中具有一些额外功能,以利用特定于 Tomcat 的 API 暂停接收消息以产生反压。

要启动 WebSocket 会话,您可以创建 Client 端的实例并使用其execute方法:

WebSocketClient client = new ReactorNettyWebSocketClient();

URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
        session.receive()
                .doOnNext(System.out::println)
                .then());

某些 Client 端(例如 Jetty)实现Lifecycle,并且需要先停止然后启动,然后才能使用它们。所有 Client 端都具有与基础 WebSocketClient 端的配置有关的构造器选项。

4. Testing

在 Spring MVC 中相同

spring-test模块提供ServerHttpRequestServerHttpResponseServerWebExchange的模拟实现。有关模拟对象的讨论,请参见Spring WebReactive

WebTestClient构建在这些模拟请求和响应对象的基础上,以提供对不使用 HTTP 服务器的 WebFlux 应用程序测试的支持。您也可以使用WebTestClient进行端到端集成测试。

5.ReactiveLibrary

spring-webflux依赖reactor-core,并在内部使用它来构成异步逻辑并提供 Reactive Streams 支持。通常,WebFlux API 返回FluxMono(因为它们在内部使用),并且宽容地接受任何 Reactive Streams Publisher实现作为 Importing。 FluxMono的使用很重要,因为它有助于表达基数,例如,是期望单个还是多个异步值,并且对于决策(例如,在编码或解码 HTTP 消息时)至关重要。

对于带 Comments 的控制器,WebFlux 透明地适应应用程序选择的反应式库。这是在ReactiveAdapterRegistry的帮助下完成的,该ReactiveAdapterRegistry提供了对反应库和其他异步类型的可插入支持。该注册表具有对 RxJava 和CompletableFuture的内置支持,但您也可以注册其他注册表。

对于功能性 API(例如Functional EndpointsWebClient等),WebFlux API 的一般规则适用于返回值FluxMono和响应流Publisher作为 Importing。当提供Publisher时,无论是自定义的还是来自其他 Reactive 库的,都只能将其视为语义未知(0..N)的流。但是,如果知道语义,则可以用FluxMono.from(Publisher)包裹它,而不用传递原始Publisher

例如,给定的Publisher不是Mono,Jackson JSON 消息编写器需要多个值。如果媒体类型暗示无限流(例如application/json+stream),则将分别写入和刷新值。否则,值将缓冲到列表中并呈现为 JSON 数组。