# 反应式堆栈上的 Web

文档的这一部分涵盖了对构建在[反应流](https://www.reactive-streams.org/)API 上的反应式堆栈 Web 应用程序的支持，该应用程序可在非阻塞服务器上运行，例如 Netty、 Undertow 和 Servlet 3.1+ 容器。个别章节涵盖了[Spring WebFlux](webflux.html#webflux)框架、反应性[`WebClient`]（#WebFlux-client）、对[testing](#webflux-test)和[反应库](#webflux-reactive-libraries)的支持。对于 Servlet-stack Web 应用程序，请参见[Web on Servlet Stack](web.html#spring-web)。

## 1. Spring WebFlux

Spring 框架中包含的原始 Web 框架 Spring Web MVC 是专门为 Servlet API 和 Servlet 容器构建的。反应式堆栈 Web 框架 Spring WebFlux 是后来在 5.0 版本中添加的。它是完全非阻塞的，支持[反应流](https://www.reactive-streams.org/)背压，并在 Netty、 Undertow 和 Servlet 3.1+ 容器等服务器上运行。

这两个 Web 框架都反映了它们的源模块的名称（[spring-webmvc](https://github.com/spring-projects/spring-framework/tree/main/spring-webmvc)和[spring-webflux](https://github.com/spring-projects/spring-framework/tree/main/spring-webflux)），并在 Spring 框架中并存。每个模块都是可选的。应用程序可以使用一个或另一个模块，或者在某些情况下，同时使用这两个模块—例如，具有 Spring MVC 控制器的反应性。

### 1.1.概述

为什么要创建 WebFlux？

部分解决方案是需要一个非阻塞的 Web 堆栈来处理少量线程的并发性，并以更少的硬件资源进行扩展。 Servlet 3.1 确实为非阻塞 I/O 提供了一个 API。但是，使用它会导致远离 Servlet API 的其余部分，其中契约是同步的（`Filter`，`Servlet`）或阻塞的（`getParameter`，`getPart`）。这就是一个新的通用 API 的动机，它可以作为跨任何非阻塞运行时的基础。这一点很重要，因为服务器（如 Netty）在异步、非阻塞空间中已经很好地建立了。

答案的另一部分是函数式编程。正如 爪哇5 中添加的注释创造了机会（例如注释的 REST 控制器或单元测试）一样，爪哇8 中添加的 lambda 表达式为 爪哇 中的功能 API 创造了机会。这对于允许异步逻辑的声明式组合的非阻塞应用程序和延续风格 API（由`CompletableFuture`和[ReactiveX](http://reactivex.io/)推广）是一个福音。在编程模型级别，爪哇8 使 Spring WebFlux 能够在带注释的控制器之外提供功能性的 Web 端点。

#### 1.1.1.定义“反应性”

我们谈到了“非阻塞”和“功能性”，但是反应性是什么意思呢？

术语“反应性”指的是围绕对变化做出反应而构建的编程模型——网络组件对 I/O 事件做出反应，UI 控制器对鼠标事件做出反应，等等。从这个意义上说，非阻塞是反应性的，因为我们现在不是被阻塞，而是在操作完成或数据可用时对通知做出反应。

我们团队中还有另一个与“反应性”相关的重要机制，那就是无阻塞背压。在同步的命令式代码中，阻塞调用作为一种自然的反压形式，迫使调用者等待。在非阻塞代码中，控制事件的速率变得很重要，这样快速生成器就不会淹没其目标。

反应流是一个[small spec](https://github.com/reactive-streams/reactive-streams-jvm/blob/master/README.md#specification)（在 爪哇9 中也是[adopted](https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html)），它定义了具有背压的异步组件之间的交互。例如，数据存储库（充当[Publisher](https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Publisher.html)）可以生成 HTTP 服务器（充当[Subscriber](https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Subscriber.html)）随后可以写入响应的数据。反应流的主要目的是让订阅者控制发布者生成数据的速度或速度。

|   |**常见问题：如果出版商不能放慢速度怎么办？**<br/>反应流的目的只是为了建立机制和边界。<br/>如果一个发布者不能减速，它就必须决定是缓冲、下降还是失败。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.1.2.反应性 API

反应流在互操作性中起着重要的作用。它是库和基础设施组件感兴趣的，但作为应用程序 API 用处不大，因为它的级别太低。应用程序需要一个更高层次和更丰富的功能 API 来组成异步逻辑——类似于 爪哇8`Stream`API，但不仅仅是用于集合。这就是反应库所扮演的角色。

[Reactor](https://github.com/reactor/reactor)是 Spring WebFlux 选择的反应库。它提供了[`Mono`]（https://projectreactor.io/DOCS/core/release/api/reactor/core/publisher/mono.html）和[`Flux`]（https://projectreactor.io/core/core/release/api/reactor/reactor/publisher.html）API 类型，以便通过与 reactivex=“388”/>对齐的一组丰富的运算符对 0..1（`Mono`）和 0.n（<gt r=“386”）的数据序列进行工作。反应器是一个反应库，因此，它的所有操作人员都支持无阻塞背压。Reactor 非常关注服务器端 爪哇。它是与 Spring 密切合作开发的。

WebFlux 需要将 Reactor 作为核心依赖项，但它可以通过反应流与其他反应库进行互操作。作为一般规则，WebFlux API 接受普通的`Publisher`作为输入，在内部将其调整为反应器类型，并使用该类型，并返回`Flux`或`Mono`作为输出。因此，你可以将任何`Publisher`作为输入传递，并且可以在输出上应用操作，但是你需要调整输出以与另一个反应库一起使用。只要可行（例如，带注释的控制器），WebFlux 就会透明地适应 RX爪哇 或其他反应库的使用。有关更多详细信息，请参见[反应库](#webflux-reactive-libraries)。

|   |除了反应性 API，WebFlux 还可以与[Coroutines](languages.html#coroutines) Kotlin 中的 API 一起使用，这提供了一种更必要的编程风格。<br/>下面的 Kotlin 代码示例将与协程 API 一起提供。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.1.3.程序设计模型

`spring-web`模块包含支撑 Spring WebFlux 的反应性基础，包括 HTTP 抽象、支持服务器的反应性流[adapters](#webflux-httphandler)、[codecs](#webflux-codecs)，以及与 Servlet API 类似但具有非阻塞契约的核心[`WebHandler`API]（#WebFlux-web-handler-API）。

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

* [带注释的控制器](#webflux-controller):与 Spring MVC 一致，并且基于来自`spring-web`模块的相同注释。 Spring MVC 和 WebFlux 控制器都支持反应（反应器和 RX爪哇）返回类型，因此，很难将它们区分开来。一个值得注意的区别是，WebFlux 还支持活性的`@RequestBody`参数。

* [功能端点](#webflux-fn):基于 lambda 的、轻量级的和函数式的编程模型。你可以将其视为一个小型的库或一组实用程序，应用程序可以使用它们来路由和处理请求。与注解控制器的最大区别在于，应用程序负责从头到尾处理请求，而不是通过注解声明意图并被回调。

#### 1.1.4.适用性

Spring MVC 还是 WebFlux？

这是个自然的问题，但会造成一种不合理的二分法。实际上，两者共同作用来扩大可供选择的范围。这两种设计是为了彼此之间的连续性和一致性，它们可以并排使用，并且来自每一方的反馈对双方都有利。下面的图表显示了这两者之间的关系，它们的共同点，以及各自的独特支持：

![spring mvc and webflux venn](images/spring-mvc-and-webflux-venn.png)

我们建议你考虑以下几点：

* 如果你的 Spring MVC 应用程序运行良好，则无需更改。命令式编程是编写、理解和调试代码的最简单的方法。你有最多的库可供选择，因为从历史上看，大多数库都是阻塞的。

* 如果你已经在寻找一个非阻塞的 Web 堆栈， Spring WebFlux 提供了与该空间中的其他程序相同的执行模型的优点，并且还提供了服务器（Netty、 Tomcat、 Jetty、 Undertow 和 Servlet 3.1+ 容器）的选择，以及编程模型的选择（带注释的控制器和功能的 Web 端点），以及反应库的选择（反应器、RX爪哇 或其他）。

* 如果你对与 爪哇8Lambdas 或 Kotlin 一起使用的轻量级、功能性 Web 框架感兴趣，那么可以使用 Spring WebFlux Functional Web Endpoints。对于较小的应用程序或需求不那么复杂的微服务来说，这也是一个很好的选择，它们可以受益于更高的透明度和控制。

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

* 评估应用程序的一种简单方法是检查其依赖关系。如果你有阻塞持久性 API（ JPA、JDBC）或网络 API 可供使用， Spring MVC 至少是通用架构的最佳选择。对于 Reactor 和 Rx爪哇 来说，在单独的线程上执行阻塞调用在技术上是可行的，但是你不会充分利用非阻塞的 Web 堆栈。

* 如果你有一个 Spring MVC 应用程序，其中调用了远程服务，请尝试 reactive`WebClient`。你可以直接从 Spring MVC 控制器方法返回反应类型（reactor，rxjava，[or other](#webflux-reactive-libraries)）。每次调用的延迟越大或调用之间的相互依赖性越大，其好处就越显著。 Spring MVC 控制器也可以调用其他无功分量。

* 如果你有一个庞大的团队，请记住，在向非阻塞、函数式和声明式编程的转变中，学习曲线很陡。在没有全开关的情况下，一种实用的启动方式是使用反应式`WebClient`。除此之外，从小处着手，衡量收益。我们预计，对于广泛的应用而言，这种转变是不必要的。如果你不确定要寻找哪些好处，那么可以从了解非阻塞 I/O 的工作方式（例如，在单线程 node.js 上的并发性）及其效果开始。

#### 1.1.5.服务器

Spring WebFlux 在 Tomcat、 Jetty、 Servlet 3.1+ 容器上以及在诸如 Netty 和 Undertow 等非 Servlet 运行时上得到支持。所有服务器都适应于低级别的[common API](#webflux-httphandler)，以便可以跨服务器支持更高级别的[程序设计模型](#webflux-programming-models)。

Spring WebFlux 不具有启动或停止服务器的内置支持。然而，很容易从 Spring 配置和[WebFlux 基础设施](#webflux-config)和[run it](#webflux-httphandler)的应用程序中使用几行代码。

Spring Boot 有一个 WebFlux 启动器，可以自动执行这些步骤。默认情况下，启动器使用 Netty，但通过更改 Maven 或 Gradle 依赖关系，很容易切换到 Tomcat、 Jetty 或 Undertow。 Spring 启动默认为 netty，因为它在异步、非阻塞空间中被更广泛地使用，并且允许客户机和服务器共享资源。

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.表现

表演有许多特点和意义。反应性和非阻塞通常不会使应用程序运行得更快。在某些情况下，它们可以（例如，如果使用`WebClient`并行运行远程调用）。总的来说，它需要更多的工作来做事情的非阻塞的方式，这可以稍微增加所需的处理时间。

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

#### 1.1.7.并发模型

Spring MVC 和 Spring WebFlux 都支持带注释的控制器，但在并发模型和用于阻塞和线程的默认假设中存在关键差异。

在 Spring MVC（和 Servlet 一般的应用程序）中，假定应用程序可以阻止当前线程，（例如，用于远程调用）。出于这个原因， Servlet 容器使用一个大的线程池来吸收请求处理过程中的潜在阻塞。

Spring 在 WebFlux（以及一般的非阻塞服务器）中，假定应用程序不会阻塞。因此，非阻塞服务器使用一个小的、固定大小的线程池（事件循环工作者）来处理请求。

|   |“可伸缩”和“少量线程”听起来可能是矛盾的，但永远不要阻塞<br/>当前线程（而是依赖回调）意味着你不需要额外的线程，因为<br/>没有要吸收的阻塞调用。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

调用阻塞 API

如果你确实需要使用阻塞库，该怎么办？Reactor 和 Rx爪哇 都提供`publishOn`操作符，以便在不同的线程上继续处理。这意味着有一个很容易逃脱的舱口。然而，请记住，阻塞 API 并不适合这种并发模型。

可变状态

在 Reactor 和 RX爪哇 中，你通过运算符声明逻辑。在运行时，会形成一个反应性管道，在该管道中，数据会在不同的阶段中按顺序进行处理。这样做的一个主要好处是，它使应用程序不必保护可变状态，因为该管道中的应用程序代码永远不会并发调用。

线程模型

在运行 Spring WebFlux 的服务器上，你应该看到哪些线程？

* 在“vanilla” Spring WebFlux 服务器上（例如，没有数据访问或其他可选的依赖关系），你可以期望为服务器提供一个线程，为请求处理提供几个线程（通常与 CPU 内核的数量一样多）。 Servlet 然而，容器可以以更多线程（例如， Tomcat 上的 10）开始，以支持 Servlet（阻塞）I/O 和 Servlet 3.1（非阻塞）I/O 的使用。

* 反应式`WebClient`以事件循环方式进行操作。因此，你可以看到与此相关的处理线程的数量很少且是固定的（例如，`reactor-http-nio-`与 Reactor Netty 连接器）。但是，如果 Reactor Netty 同时用于客户机和服务器，则默认情况下这两个服务器共享事件循环资源。

* Reactor 和 Rx爪哇 提供线程池抽象（称为调度器），与`publishOn`操作符一起使用，该操作符用于将处理切换到不同的线程池。调度程序的名称建议了一种特定的并发策略——例如，“并行”（用于线程数量有限的 CPU 绑定工作）或“弹性”（用于具有大量线程的 I/O 绑定工作）。如果你看到这样的线程，这意味着某些代码正在使用特定的线程池`Scheduler`策略。

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

配置

Spring 框架不提供对启动和停止[servers](#webflux-server-choice)的支持。要为服务器配置线程模型，你需要使用特定于服务器的配置 API，或者，如果你使用 Spring 引导，请检查每个服务器的 Spring 引导配置选项。你可以直接[configure](#webflux-client-builder)`WebClient`。对于所有其他库，请参阅它们各自的文档。

### 1.2.反应核

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

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

  * [Httphandler](#webflux-httphandler):使用非阻塞 I/O 和反应流反压处理 HTTP 请求的基本契约，以及用于反应堆网络、 Undertow、 Tomcat、 Jetty 和任何 Servlet 3.1+ 容器的适配器。

  * [`WebHandler`API]（#WebFlux-Web-Handler-API）：略高级别的、用于请求处理的通用 Web API，在此基础上构建了具体的编程模型，如带注释的控制器和功能端点。

* 对于客户端，有一个基本的`ClientHttpConnector`契约来执行具有非阻塞 I/O 和反应性流反压的 HTTP 请求，以及用于[反应堆网状结构](https://github.com/reactor/reactor-netty)、反应性[Jetty HttpClient](https://github.com/jetty-project/jetty-reactive-httpclient)和[Apache HttpComponents](https://hc.apache.org/)的适配器。应用程序中使用的较高级别[WebClient](#webflux-client)建立在此基本契约上。

* 对于客户机和服务器，[codecs](#webflux-codecs)用于序列化和反序列化 HTTP 请求和响应内容。

#### 1.2.1.`HttpHandler`

[Httphandler](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/http/server/reactive/HttpHandler.html)是一个简单的契约，它只有一个方法来处理请求和响应。它是故意最小化的，它的主要目的也是唯一的目的是在不同的 HTTP 服务器 API 上进行最小化抽象。

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

|     Server name     |使用的服务器 API|                     Reactive Streams support                      |
|---------------------|--------------------------------------------------------------------------------|-------------------------------------------------------------------|
|        Netty        |Netty API|     [Reactor Netty](https://github.com/reactor/reactor-netty)     |
|      Undertow       |Undertow 空气污染指数|          spring-web: Undertow to Reactive Streams bridge          |
|       Tomcat        |Servlet 3.1 非阻塞 I/O； Tomcat 读写字节缓冲器 VS 字节的 API[]|spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge|
|        Jetty        |Servlet 3.1 非阻塞 I/O； Jetty 写字节缓冲器 VS 字节的 API[]|spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge|
|Servlet 3.1 container|Servlet 3.1 非阻塞 I/O|spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge|

下表描述了服务器的依赖关系（另请参见[支持的版本](https://github.com/spring-projects/spring-framework/wiki/What%27s-New-in-the-Spring-Framework)）：

| Server name |       Group id        |工件名称|
|-------------|-----------------------|---------------------------|
|Reactor Netty|io.projectreactor.netty|反应堆网状结构|
|  Undertow   |      io.undertow      |Undertow-核心|
|   Tomcat    |org.apache.tomcat.embed|Tomcat-嵌入-核心|
|    Jetty    |   org.eclipse.jetty   |Jetty-服务器， Jetty- Servlet|

下面的代码片段显示了在每个服务器 API 中使用`HttpHandler`适配器的情况：

**反应堆网状结构**

爪哇

```
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();
```

Kotlin

```
val handler: HttpHandler = ...
val adapter = ReactorHttpHandlerAdapter(handler)
HttpServer.create().host(host).port(port).handle(adapter).bind().block()
```

**Undertow**

爪哇

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

Kotlin

```
val handler: HttpHandler = ...
val adapter = UndertowHttpHandlerAdapter(handler)
val 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();
```

Kotlin

```
val handler: HttpHandler = ...
val servlet = TomcatHttpHandlerAdapter(handler)

val server = Tomcat()
val base = File(System.getProperty("java.io.tmpdir"))
val rootContext = server.addContext("", base.absolutePath)
Tomcat.addServlet(rootContext, "main", servlet)
rootContext.addServletMappingDecoded("/", "main")
server.host = host
server.setPort(port)
server.start()
```

**Jetty**

爪哇

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

Kotlin

```
val handler: HttpHandler = ...
val servlet = JettyHttpHandlerAdapter(handler)

val server = Server()
val contextHandler = ServletContextHandler(server, "")
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();

val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()
```

**Servlet 3.1+ Container**

要将 WAR 部署到任何 Servlet 3.1+ 容器上，你可以在 WAR 中扩展并包括[`AbstractReactiveWebInitializer`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/server/adapter/abstractreactivewebinitializer.html）。该类将`HttpHandler`与`ServletHttpHandlerAdapter`包装在一起，并将其注册为`Servlet`。

#### 1.2.2.`WebHandler`api

`org.springframework.web.server`包构建在[`HttpHandler`]（#WebFlux-Httphandler）合同的基础上，通过多个[`WebExceptionHandler`]（https://DOCS. Spring.io/ Spring-Framework/DOCS/5.3.16/javadoc-api/org/org/org/SpringFramework/Web/Server/WebExceptionHandler.html）的链提供一个通用的 Web API，用于处理请求（https:///gt://gt/DOCS. Spring.3.3.org/16.org/16.org/javloverFramework.org/jumerfilter 通过简单地指向一个 Spring `ApplicationContext`，其中组件是[自动检测](#webflux-web-handler-api-special-beans)，和/或通过向构建器注册组件，可以将链与`WebHttpHandlerBuilder`放在一起。

虽然`HttpHandler`的一个简单目标是抽象不同 HTTP 服务器的使用，但`WebHandler`API 的目标是提供 Web 应用程序中常用的一组更广泛的功能，例如：

* 具有属性的用户会话。

* 请求属性。

* 已为请求解析`Locale`或`Principal`。

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

* 多部分数据的抽象。

* 还有更多..

##### 特殊 Bean 类型

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

|         Bean name          |         Bean type          |Count|说明|
|----------------------------|----------------------------|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|          \<any\>           |   `WebExceptionHandler`    |0..N |为来自`WebFilter`实例和目标`WebHandler`的链中的异常提供处理。有关更多详细信息，请参见[Exceptions](#webflux-exception-handler)。|
|          \<any\>           |        `WebFilter`         |0..N |将截取样式逻辑应用于过滤器链的其余部分之前和之后，以及<br/>目标`WebHandler`。有关更多详细信息，请参见[Filters](#webflux-filters)。|
|        `webHandler`        |        `WebHandler`        |  1  |请求的处理程序。|
|    `webSessionManager`     |    `WebSessionManager`     |0..1 |默认情况下，`WebSession`实例的管理器通过`ServerWebExchange`上的方法公开。`DefaultWebSessionManager`实例。|
|  `serverCodecConfigurer`   |  `ServerCodecConfigurer`   |0..1 |用于访问`HttpMessageReader`实例以解析表单数据和多部分数据，然后<br/>通过`ServerWebExchange`上的方法公开。默认情况下`ServerCodecConfigurer.create()`。|
|  `localeContextResolver`   |  `LocaleContextResolver`   |0..1 |默认情况下，`LocaleContext`的解析器通过`ServerWebExchange`上的方法公开。`AcceptHeaderLocaleContextResolver`。|
|`forwardedHeaderTransformer`|`ForwardedHeaderTransformer`|0..1 |对于处理转发的类型头，可以通过提取和删除它们，也可以只删除它们。<br/>默认情况下不使用。|

##### 表单数据

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

爪哇

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

Kotlin

```
suspend fun getFormData(): MultiValueMap<String, String>
```

`DefaultServerWebExchange`使用配置的`HttpMessageReader`将表单数据（`application/x-www-form-urlencoded`）解析为`MultiValueMap`。默认情况下，`FormHttpMessageReader`被配置为由`ServerCodecConfigurer` Bean 使用（请参见[Web 处理程序 API](#webflux-web-handler-api)）。

##### 多部分数据

[Web MVC](web.html#mvc-multipart)

`ServerWebExchange`公开了以下访问多部分数据的方法：

爪哇

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

Kotlin

```
suspend fun getMultipartData(): MultiValueMap<String, Part>
```

`DefaultServerWebExchange`使用配置的`HttpMessageReader<MultiValueMap<String, Part>>`将`multipart/form-data`内容解析为`MultiValueMap`。默认情况下，这是`DefaultPartHttpMessageReader`，它没有任何第三方依赖关系。或者，可以使用`SynchronossPartHttpMessageReader`，这是基于[Synchronoss 多部件蔚来](https://github.com/synchronoss/nio-multipart)库的。这两个参数都是通过`ServerCodecConfigurer` Bean 配置的（参见[Web 处理程序 API](#webflux-web-handler-api)）。

要以流式方式解析多部分数据，可以使用从`HttpMessageReader<Part>`返回的`Flux<Part>`代替。例如，在带注释的控制器中，使用`@RequestPart`意味着通过名称对各个部分进行类似`Map`的访问，因此需要完整地解析多部分数据。相比之下，你可以使用`@RequestBody`将内容解码为`Flux<Part>`，而无需收集到`MultiValueMap`。

##### 转发头

[Web MVC](web.html#filters-forwarded-headers)

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

[RFC 7239](https://tools.ietf.org/html/rfc7239)定义了`Forwarded`HTTP 报头，代理可以使用该报头来提供有关原始请求的信息。也有其他非标准标题，包括`X-Forwarded-Host`、`X-Forwarded-Port`、`X-Forwarded-Proto`、`X-Forwarded-Ssl`和`X-Forwarded-Prefix`。

`ForwardedHeaderTransformer`是一个组件，它基于转发的标头修改请求的主机、端口和方案，然后删除这些标头。如果将其声明为 Bean，并使用名称`forwardedHeaderTransformer`，则将其声明为[detected](#webflux-web-handler-api-special-beans)。

转发头的安全性需要考虑，因为应用程序不能知道头是由代理添加的，还是由恶意客户机添加的。这就是为什么在信任边界上的代理应该被配置为删除来自外部的不受信任的转发流量。你还可以将`ForwardedHeaderTransformer`配置为`removeOnly=true`，在这种情况下，它会删除但不使用头。

|   |在 5.1 中，`ForwardedHeaderFilter`被弃用，并被`ForwardedHeaderTransformer`取代，因此在创建<br/>交换之前，可以更早地处理转发头。如果无论如何都配置了过滤器，则将其从<br/>过滤器列表中取出，并使用`ForwardedHeaderTransformer`代替。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.2.3.过滤器

[Web MVC](web.html#filters)

在[`WebHandler`API]（#WebFlux-Web-Handler-API）中，可以使用`WebFilter`在过滤器和目标`WebHandler`的处理链的其余部分之前和之后应用拦截风格的逻辑。当使用[WebFlux 配置](#webflux-config)时，注册`WebFilter`就像将其声明为 Spring  Bean 一样简单，并且（可选地）通过在 Bean 声明上使用`@Order`或通过实现`Ordered`来表示优先级。

##### CORS

[Web MVC](web.html#filters-cors)

Spring WebFlux 通过控制器上的注释为 CORS 配置提供了细粒度的支持。然而，当你将其与 Spring 安全性一起使用时，我们建议依赖于内置的`CorsFilter`，这必须在 Spring 安全性的过滤器链之前订购。

有关更多详细信息，请参见[CORS](#webflux-cors)和[webflux-cors.html](webflux-cors.html#webflux-cors-webfilter)一节。

#### 1.2.4.例外

[Web MVC](web.html#mvc-ann-customer-servlet-container-error-page)

在[`WebHandler`API]中，可以使用`WebExceptionHandler`来处理来自`WebFilter`实例和目标`WebHandler`的链中的异常。当使用[WebFlux 配置](#webflux-config)时，注册`WebExceptionHandler`就像将其声明为 Spring  Bean 一样简单，并且（可选地）通过在 Bean 声明上使用`@Order`或通过实现`Ordered`来表示优先级。

下表描述了可用的`WebExceptionHandler`实现：

|           Exception Handler           |说明|
|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|   `ResponseStatusExceptionHandler`    |通过将响应设置为异常的 HTTP 状态代码，为类型[`ResponseStatusException`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/server/responsestatusception.html）的异常提供处理。|
|`WebFluxResponseStatusExceptionHandler`|`ResponseStatusExceptionHandler`的扩展，该扩展还可以在任何异常情况下确定<br/>代码的 HTTP 状态`@ResponseStatus`注释。<br/><br/>在[WebFlux 配置](#webflux-config)中声明此处理程序。|

#### 1.2.5.编解码器

[Web MVC](integration.html#rest-message-conversion)

`spring-web`和`spring-core`模块通过具有反应流反压力的非阻塞 I/O，提供对与高层对象之间的字节内容的序列化和反序列化的支持。以下介绍了这种支持：

* [`Encoder`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/codec/encoder.html）和[`Decoder`（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/core/core/codecoder.html）是独立于 http 的编码和解码内容的低级合同。

* [`HttpMessageReader`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/http/codec/httpMessageReader.html）和[`HttpMessageWriter`（https://DOCS. Spring.io/ Spring/ Spring-framework/DOCS/5.3.16/javadoc-api/org/spramework/http/codec/httpmessagewriter.html）是对 HTTPMessageWriter 内容进行编码和解码的合同。

* `Encoder`可以用`EncoderHttpMessageWriter`包装，以使其适合在 Web 应用程序中使用，而`Decoder`可以用`DecoderHttpMessageReader`包装。

* [`DataBuffer`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/io/io/buffer/databuffer.html）抽象出不同的字节缓冲区表示（例如 netty`ByteBuf`，`java.nio.ByteBuffer`等），这也是所有编解码器的工作内容。有关此主题的更多信息，请参见“ Spring core”部分中的[数据缓冲区和编解码器](core.html#databuffers)。

`spring-core`模块提供`byte[]`、`ByteBuffer`、`DataBuffer`、`Resource`和`String`编码器和解码器实现方式。`spring-web`模块提供了 Jackson 的 JSON、JacksonSmile、JAXB2、协议缓冲区和其他编码器和解码器，以及用于表单数据、多部分内容、服务器发送的事件等的仅用于 Web 的 HTTP 消息阅读器和编写器实现。

`ClientCodecConfigurer`和`ServerCodecConfigurer`通常用于配置和定制要在应用程序中使用的编解码器。参见关于配置[HTTP 消息编解码器](#webflux-config-message-codecs)的部分。

##### JacksonJSON

当 Jackson 库存在时，都支持 JSON 和二进制 JSON（[Smile](https://github.com/FasterXML/smile-format-specification)）。

`Jackson2Decoder`的工作原理如下：

* Jackson 的异步、非阻塞解析器用于将一个字节块流聚合到`TokenBuffer`中，每个字节块代表一个 JSON 对象。

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

* 当解码到单值发布器（例如`Mono`）时，存在一个`TokenBuffer`。

* 当解码到多值发布者（例如`Flux`）时，一旦接收到用于完全形成的对象的足够字节，每个`TokenBuffer`都会传递到`ObjectMapper`。输入内容可以是 JSON 数组，或任何[线分隔的 JSON](https://en.wikipedia.org/wiki/JSON_streaming)格式，例如 NDJSON、JSON 行或 JSON 文本序列。

`Jackson2Encoder`的工作原理如下：

* 对于单个值发布者（例如`Mono`），只需通过`ObjectMapper`序列化它。

* 对于具有`application/json`的多值发布者，默认情况下，使用`Flux#collectToList()`收集这些值，然后序列化生成的集合。

* 对于具有流媒体类型（如`application/x-ndjson`或`application/stream+x-jackson-smile`）的多值发布者，使用[线分隔的 JSON](https://en.wikipedia.org/wiki/JSON_streaming)格式对每个值分别进行编码、写入和刷新。其他流媒体类型可以在编码器中注册。

* 对于 SSE，每个事件都调用`Jackson2Encoder`，并刷新输出，以确保及时交付。

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

##### 表单数据

`FormHttpMessageReader`和`FormHttpMessageWriter`支持解码和编码`application/x-www-form-urlencoded`内容。

在表单内容经常需要从多个地方访问的服务器端，`ServerWebExchange`提供了一个专用的`getFormData()`方法，该方法通过`FormHttpMessageReader`解析内容，然后缓存结果以进行重复访问。参见[`WebHandler`API]部分中的[Form Data](#webflux-form-data)。

一旦使用`getFormData()`，就不能再从请求主体中读取原始 RAW 内容。出于这个原因，应用程序需要始终通过`ServerWebExchange`来访问缓存的表单数据，而不是从原始请求主体读取数据。

##### 多部分

`MultipartHttpMessageReader`和`MultipartHttpMessageWriter`支持解码和编码“multipart/form-data”内容。反过来，`MultipartHttpMessageReader`将实际解析委托给另一个`HttpMessageReader`，然后简单地将部分收集到`MultiValueMap`中。默认情况下，使用`DefaultPartHttpMessageReader`，但这可以通过`ServerCodecConfigurer`进行更改。有关`DefaultPartHttpMessageReader`的更多信息，请参阅`DefaultPartHttpMessageReader`的[javadoc]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/http/codec/multipart/defaultparthpmessagereader.html）。

在可能需要从多个地方访问多部分表单内容的服务器端，`ServerWebExchange`提供了一个专用的`getMultipartData()`方法，该方法通过`MultipartHttpMessageReader`解析内容，然后缓存结果以进行重复访问。参见[`WebHandler`API]部分中的[多部分数据](#webflux-multipart)。

一旦使用`getMultipartData()`，就不能再从请求主体中读取原始 RAW 内容。由于这个原因，应用程序必须始终使用`getMultipartData()`进行重复的、类似于 MAP 的部分访问，或者以其他方式依赖于`SynchronossPartHttpMessageReader`进行一次性访问`Flux<Part>`。

##### 限制

`Decoder`和`HttpMessageReader`实现了对部分或全部输入流进行缓冲，可以在内存中配置缓冲的最大字节数的限制。在某些情况下，发生缓冲是因为输入被聚合并表示为单个对象——例如，带有`@RequestBody byte[]`、`x-www-form-urlencoded`数据的控制器方法，以此类推。在分割输入流（例如，分隔的文本、JSON 对象流等）时，流也可以发生缓冲。对于那些流情况，限制应用于与流中的一个对象相关联的字节数。

要配置缓冲区大小，你可以检查给定的`Decoder`或`HttpMessageReader`是否公开了`maxInMemorySize`属性，如果是这样，爪哇doc 将提供有关默认值的详细信息。在服务器端，`ServerCodecConfigurer`提供了一个设置所有编解码器的位置，请参见[HTTP 消息编解码器](#webflux-config-message-codecs)。在客户端，所有编解码器的限制可以在[Webclient.builder](#webflux-client-builder-maxinmemorysize)中进行更改。

对于[多部分解析](#webflux-codecs-multipart)，`maxInMemorySize`属性限制了非文件部分的大小。对于文件部件，它确定将部件写入磁盘的阈值。对于写入磁盘的文件部件，有一个额外的`maxDiskUsagePerPart`属性来限制每个部件的磁盘空间。还有一个`maxParts`属性来限制多部分请求中的部分总数。要在 WebFlux 中配置这三个变量，你需要提供一个`MultipartHttpMessageReader`到`ServerCodecConfigurer`的预配置实例。

##### 流媒体

[Web MVC](web.html#mvc-ann-async-http-streaming)

当流到 HTTP 响应时（例如，`text/event-stream`，`application/x-ndjson`），定期发送数据是很重要的，以便可靠地检测断开连接的客户端，越早越好。这样的发送可能是一个只有评论的、空的 SSE 事件，或者是任何其他可以有效充当心跳的“无操作”数据。

##### `DataBuffer`

`DataBuffer`是 WebFlux 中字节缓冲区的表示形式。 Spring 该引用的核心部分在[数据缓冲区和编解码器](core.html#databuffers)一节中有更多关于该引用的内容。要理解的关键点是，在一些服务器（如 Netty）上，字节缓冲区是池的，引用也是计算的，并且必须在使用时释放，以避免内存泄漏。

WebFlux 应用程序通常不需要关注这些问题，除非它们直接使用或产生数据缓冲区，而不是依赖编解码器来转换到更高级别的对象，或者除非它们选择创建自定义编解码器。对于这种情况，请参阅[数据缓冲区和编解码器](core.html#databuffers)中的信息，特别是关于[使用 Databuffer](core.html#databuffers-using)的部分。

#### 1.2.6.伐木

[Web MVC](web.html#mvc-logging)

Spring WebFlux 中的`DEBUG`级别日志被设计为紧凑、最小且对人类友好的。它关注的是一次又一次有用的高价值信息，而不是仅在调试特定问题时有用的其他信息。

`TRACE`级别日志记录通常遵循与`DEBUG`相同的原则（例如，也不应该是消防软管），但可以用于调试任何问题。此外，一些日志消息可能在`TRACE`与`DEBUG`处显示不同级别的详细信息。

良好的日志记录来自于使用日志的经验。如果你发现任何不符合规定的目标，请告诉我们。

##### 日志 ID

在 WebFlux 中，单个请求可以在多个线程上运行，而线程 ID 对于关联属于特定请求的日志消息是没有用的。这就是为什么 WebFlux 日志消息在默认情况下使用特定于请求的 ID 作为前缀的原因。

在服务器端，日志 ID 存储在`ServerWebExchange`属性中（[`LOG_ID_ATTRIBUTE`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/server/serverwebexchange.html#log_id_attribute）），而基于该 ID 的完全格式化的前缀可从`ServerWebExchange#getLogPrefix()`获得。在`WebClient`端，日志 ID 存储在`ClientRequest`属性（[`LOG_ID_ATTRIBUTE`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/active/function/client/clientrequest.html#log_id_attribute）中），而完全格式化的前缀可从`ClientRequest#logPrefix()`获得。

##### 敏感数据

[Web MVC](web.html#mvc-logging-sensitive-data)

`DEBUG`和`TRACE`日志记录可以记录敏感信息。这就是为什么表单参数和标题在默认情况下是屏蔽的，并且你必须显式地完全启用它们的日志记录。

下面的示例展示了如何为服务器端请求执行此操作：

爪哇

```
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {

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

Kotlin

```
@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        configurer.defaultCodecs().enableLoggingRequestDetails(true)
    }
}
```

下面的示例展示了如何为客户端请求执行此操作：

爪哇

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

WebClient webClient = WebClient.builder()
        .exchangeStrategies(strategies -> strategies.codecs(consumer))
        .build();
```

Kotlin

```
val consumer: (ClientCodecConfigurer) -> Unit  = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }

val webClient = WebClient.builder()
        .exchangeStrategies({ strategies -> strategies.codecs(consumer) })
        .build()
```

##### 附录

SLF4j 和 log4j2 等日志记录库提供了避免阻塞的异步记录器。尽管这些方法有其自身的缺点，比如可能会丢弃无法排队记录的消息，但它们是当前在反应性、非阻塞应用程序中使用的最佳可用选项。

##### 自定义编解码器

应用程序可以注册用于支持其他媒体类型的定制编解码器，或者默认编解码器不支持的特定行为。

开发人员表示的一些配置选项是在默认的编解码器上强制执行的。自定义编解码器可能希望有机会与这些首选项保持一致，比如[强制缓冲限制](#webflux-codecs-limits)或[记录敏感数据](#webflux-logging-sensitive-data)。

下面的示例展示了如何为客户端请求执行此操作：

爪哇

```
WebClient webClient = WebClient.builder()
        .codecs(configurer -> {
                CustomDecoder decoder = new CustomDecoder();
                configurer.customCodecs().registerWithDefaultConfig(decoder);
        })
        .build();
```

Kotlin

```
val webClient = WebClient.builder()
        .codecs({ configurer ->
                val decoder = CustomDecoder()
                configurer.customCodecs().registerWithDefaultConfig(decoder)
         })
        .build()
```

### 1.3.`DispatcherHandler`

[Web MVC](web.html#mvc-servlet)

Spring WebFlux，类似于 Spring MVC，是围绕前控制器模式设计的，其中中心，，提供用于请求处理的共享算法，而实际工作是通过可配置的、委托的组件来执行的。这个模型是灵活的，并支持不同的工作流程。

`DispatcherHandler`从 Spring 配置中发现它需要的委托组件。它本身也被设计为 Spring  Bean 并且实现`ApplicationContextAware`以访问它运行的上下文。如果`DispatcherHandler`是以 Bean 名`webHandler`声明的，那么它又是由[`WebHttpHandlerBuilder`]（https:/DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/server/adapter/webhtphandlerbuilder.html）发现的，它组合了一个请求-处理链，如[`WebHandler`r="webpi-api-handler.html]（#-flux）中所述。

Spring WebFlux 应用程序中的配置通常包括：

* `DispatcherHandler`与 Bean 名称`webHandler`

* `WebFilter`和`WebExceptionHandler`beans

* [`DispatcherHandler`Special Beans]（#WebFlux-Special- Bean-types）

* 其他

将配置给`WebHttpHandlerBuilder`以构建处理链，如下例所示：

爪哇

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

Kotlin

```
val context: ApplicationContext = ...
val handler = WebHttpHandlerBuilder.applicationContext(context).build()
```

得到的`HttpHandler`可以与[服务器适配器](#webflux-httphandler)一起使用了。

#### 1.3.1.特殊类型 Bean

[Web MVC](web.html#mvc-servlet-special-bean-types)

`DispatcherHandler`将委托给特殊的 bean 来处理请求并呈现适当的响应。我们所说的“特殊 bean”是指实现 WebFlux 框架契约的 Spring-managed`Object`实例。这些通常带有内置契约，但你可以自定义它们的属性，扩展它们或替换它们。

下表列出了`DispatcherHandler`检测到的特殊 bean。请注意，在较低的级别上还检测到一些其他 bean（请参见 Web 处理程序 API 中的[Special bean types](#webflux-web-handler-api-special-beans)）。

|      Bean type       |解释|
|----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|   `HandlerMapping`   |将请求映射到处理程序。该映射是基于一些条件，其中<br/>的细节通过`HandlerMapping`实现—注释控制器，简单<br/>URL 模式映射，以及其他不同的实现。<br/>`HandlerMapping`主要的`RequestMappingHandlerMapping`实现为`@RequestMapping`注释方法，`RouterFunctionMapping`为功能端点<710"/>r= 路由，和`SimpleUrlHandlerMapping`用于显式注册 URI 路径模式<br/>和`WebHandler`实例。|
|   `HandlerAdapter`   |帮助`DispatcherHandler`调用映射到请求的处理程序，而不管<br/>实际调用处理程序的方式如何。例如，调用带注释的控制器<br/>需要解析注释。a`HandlerAdapter`的主要目的是保护`DispatcherHandler`不受这些细节的影响。|
|`HandlerResultHandler`|处理来自处理程序调用的结果并最终确定响应。<br/>参见[结果处理](#webflux-resulthandling)。|

#### 1.3.2.WebFlux 配置

[Web MVC](web.html#mvc-servlet-config)

应用程序可以声明处理请求所需的基础设施 bean（在[Web 处理程序 API](#webflux-web-handler-api-special-beans)和[`DispatcherHandler`]（#Webflux-special- Bean-types）下列出）。然而，在大多数情况下，[WebFlux 配置](#webflux-config)是最好的起点。它声明所需的 bean，并提供一个更高级的配置回调 API 来定制它。

|   |Spring 启动依赖于 WebFlux 配置来配置 Spring WebFlux，并且还提供了<br/>许多额外的方便选项。|
|---|-------------------------------------------------------------------------------------------------------------------------|

#### 1.3.3.处理

[Web MVC](web.html#mvc-servlet-sequence)

`DispatcherHandler`按以下方式处理请求：

* 每个`HandlerMapping`都被要求找到一个匹配的处理程序，并使用第一个匹配。

* 如果找到了一个处理程序，则通过一个适当的`HandlerAdapter`运行该处理程序，该处理程序将执行时的返回值公开为`HandlerResult`。

* 将`HandlerResult`赋予适当的`HandlerResultHandler`，以通过直接写入响应或通过使用视图来呈现来完成处理。

#### 1.3.4.结果处理

通过`HandlerAdapter`调用处理程序的返回值被包装为`HandlerResult`，以及一些附加的上下文，并传递给声称支持它的第一个`HandlerResultHandler`。下表显示了可用的`HandlerResultHandler`实现，所有这些实现都在[WebFlux 配置](#webflux-config)中声明：

|     Result Handler Type     |返回值|   Default Order   |
|-----------------------------| |-------------------|
|`ResponseEntityResultHandler`|`ResponseEntity`，通常来自`@Controller`实例。|         0         |
|`ServerResponseResultHandler`|`ServerResponse`，通常来自功能端点。|         0         |
| `ResponseBodyResultHandler` |处理来自`@ResponseBody`方法或`@RestController`类的返回值。|        100        |
|`ViewResolutionResultHandler`|`CharSequence`，[`View`]（https://DOCS. Spring.io/ Spring-framework/5.3.16/javadoc-api/org/springframework/web/result/result/view/view.html），[Model](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/ui/Model.html)，`Map`，[Rendering](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/result/view/Rendering.html)，或其他任何`Object`均作为模型属性处理。<br/><>[视图分辨率](#webflux-viewresolution)<<>r=">><<|`Integer.MAX_VALUE`|

#### 1.3.5.例外

[Web MVC](web.html#mvc-exceptionhandlers)

从`HandlerAdapter`返回的`HandlerResult`可以基于某些特定于处理程序的机制公开用于错误处理的函数。如果出现以下情况，则调用此错误函数：

* 处理程序（例如，`@Controller`）调用失败。

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

错误函数可以更改响应（例如，到错误状态），只要在从处理程序返回的反应类型产生任何数据项之前发生错误信号。

这就是`@Controller`类中的`@ExceptionHandler`方法的支持方式。相比之下，对 Spring MVC 中相同内容的支持是建立在`HandlerExceptionResolver`上的。这一点一般不会有什么影响。但是，请记住，在 WebFlux 中，不能使用`@ControllerAdvice`来处理在选择处理程序之前发生的异常。

另请参见“注释控制器”部分中的[管理异常](#webflux-ann-controller-exceptions)或 WebHandler API 部分中的[Exceptions](#webflux-exception-handler)。

#### 1.3.6.视图分辨率

[Web MVC](web.html#mvc-viewresolver)

视图分辨率允许使用 HTML 模板和模型在浏览器上进行呈现，而无需将你绑定到特定的视图技术。在 Spring WebFlux 中，通过专用的[HandlerResultHandler](#webflux-resulthandling)支持视图解析，该实例使用`ViewResolver`实例将字符串（代表逻辑视图名称）映射到`View`实例。然后使用`View`来呈现响应。

##### 处理

[Web MVC](web.html#mvc-handling)

传递到`ViewResolutionResultHandler`中的`HandlerResult`包含来自处理程序的返回值和包含在请求处理过程中添加的属性的模型。返回值被处理为以下内容之一：

* `String`，`CharSequence`:要通过配置的`ViewResolver`实现的列表解析为`View`的逻辑视图名称。

* `void`:根据请求路径选择一个默认的视图名称，减去前导和后导斜杠，并将其解析为`View`。当未提供视图名称（例如，返回了 model 属性）或异步返回值（例如，`Mono`完全为空）时，也会发生同样的情况。

* [Rendering](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/result/view/Rendering.html):视图解析场景的 API。探索你的 IDE 中的代码补全选项。

* `Model`，`Map`:要为请求添加到模型中的额外模型属性。

* 任何其他：任何其他返回值（简单类型除外，由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定）被视为要添加到模型中的模型属性。属性名是通过使用[惯例](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/Conventions.html)从类名派生出来的，除非存在处理程序方法`@ModelAttribute`注释。

该模型可以包含异步的、反应性的类型（例如，来自 Reactor 或 RX爪哇）。在呈现之前，`AbstractView`将这些模型属性解析为具体的值并更新模型。单值活性类型被解析为单值或无值（如果为空），而多值活性类型（例如，`Flux<T>`）被收集并解析为`List<T>`。

要配置视图分辨率就像在 Spring 配置中添加`ViewResolutionResultHandler` Bean 一样简单。[WebFlux 配置](#webflux-config-view-resolvers)为视图分辨率提供了专用的配置 API。

有关与 Spring WebFlux 集成的视图技术的更多信息，请参见[查看技术](#webflux-view)。

##### 重定向

[Web MVC](web.html#mvc-redirecting-redirect-prefix)

视图名称中的特殊`redirect:`前缀允许你执行重定向。`UrlBasedViewResolver`（和子类）将其视为需要重定向的指令。视图名称的其余部分是重定向 URL。

净效果与控制器返回`RedirectView`或`Rendering.redirectTo("abc").build()`相同，但现在控制器本身可以根据逻辑视图名称进行操作。像`redirect:/some/resource`这样的视图名称是相对于当前应用程序的，而像`redirect:https://example.com/arbitrary/path`这样的视图名称会重定向到一个绝对 URL。

##### 内容协商

[Web MVC](web.html#mvc-multiple-representations)

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

为了支持诸如 JSON 和 XML 等媒体类型， Spring WebFlux 提供了`HttpMessageWriterView`，这是一个特殊的`View`，它通过[HttpMessageWriter](#webflux-codecs)呈现。通常，你会通过[WebFlux 配置](#webflux-config-view-resolvers)将这些视图配置为默认视图。如果默认视图匹配所请求的媒体类型，则始终选择并使用它们。

### 1.4.带注释的控制器

[Web MVC](web.html#mvc-controller)

Spring WebFlux 提供了一种基于注释的编程模型，其中`@Controller`和`@RestController`组件使用注释来表示请求映射、请求输入、处理异常等。带注释的控制器具有灵活的方法签名，不需要扩展基类，也不需要实现特定的接口。

下面的清单展示了一个基本示例：

Java

```
@RestController
public class HelloController {

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

Kotlin

```
@RestController
class HelloController {

    @GetMapping("/hello")
    fun handle() = "Hello WebFlux"
}
```

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

#### 1.4.1.`@Controller`

[Web MVC](web.html#mvc-ann-controller)

你可以通过使用标准的 Spring  Bean 定义来定义控制器 bean。该原型允许自动检测并与 Spring 用于检测 Classpath 中的类的通用支持保持一致，并为它们自动注册 Bean 定义。它还充当带注释的类的原型，指示其作为 Web 组件的角色。

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

Java

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

    // ...
}
```

|**1**|扫描`org.example.web`包。|
|-----|-----------------------------------|

Kotlin

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

    // ...
}
```

|**1**|扫描`org.example.web`包。|
|-----|-----------------------------------|

`@RestController`是一个[组合注释](core.html#beans-meta-annotations)，它本身用`@Controller`和`@ResponseBody`进行了元注释，表示一个控制器，其每个方法都继承了类型级`@ResponseBody`注释，因此，它直接写到响应主体与视图解析之间，并使用 HTML 模板进行呈现。

#### 1.4.2.请求映射

[Web MVC](web.html#mvc-ann-requestmapping)

`@RequestMapping`注释用于将请求映射到控制器方法。它具有各种属性，可以通过 URL、HTTP 方法、请求参数、标头和媒体类型进行匹配。你可以在类级别上使用它来表示共享映射，或者在方法级别上使用它来缩小到特定的端点映射。

还有`@RequestMapping`的特定于 HTTP 方法的快捷方式变体：

* `@GetMapping`

* `@PostMapping`

* `@PutMapping`

* `@DeleteMapping`

* `@PatchMapping`

提供前面的注释是[自定义注释](#webflux-ann-requestmapping-composed)，因为可以说，大多数控制器方法应该映射到特定的 HTTP 方法，而不是使用`@RequestMapping`，默认情况下，该方法匹配所有的 HTTP 方法。同时，在类级别上仍然需要一个`@RequestMapping`来表示共享映射。

下面的示例使用类型和方法级别映射：

Java

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

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

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

Kotlin

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

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

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun add(@RequestBody person: Person) {
        // ...
    }
}
```

##### URI 模式

[Web MVC](web.html#mvc-ann-requestmapping-uri-templates)

你可以使用 GLOB 模式和通配符来映射请求：

|    Pattern    |                                              Description                                              |例子|
|---------------|-------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|      `?`      |                                         Matches one character                                         |`"/pages/t?st.html"`匹配`"/pages/test.html"`和`"/pages/t3st.html"`|
|      `*`      |                         Matches zero or more characters within a path segment                         |`"/resources/*.png"`匹配`"/resources/file.png"`<br/><br/>匹配`"/projects/*/versions"`但不匹配`"/projects/spring/versions"`|
|     `**`      |                     Matches zero or more path segments until the end of the path                      |`"/resources/**"`匹配`"/resources/file.png"`和`"/resources/images/file.png"`<br/><br/>`"/resources/**/file.png"`是无效的，因为`**`只允许在路径的末尾。|
|   `{name}`    |                   Matches a path segment and captures it as a variable named "name"                   |`"/projects/{project}/versions"`匹配`"/projects/spring/versions"`并捕获`project=spring`|
|`{name:[a-z]+}`|                     Matches the regexp `"[a-z]+"` as a path variable named "name"                     |`"/projects/{project:[a-z]+}/versions"`匹配`"/projects/spring/versions"`但不匹配`"/projects/spring1/versions"`|
|   `{*path}`   |Matches zero or more path segments until the end of the path and captures it as a variable named "path"|`"/resources/{*file}"`匹配`"/resources/images/file.png"`并捕获`file=/images/file.png`|

可以使用`@PathVariable`访问捕获的 URI 变量，如下例所示：

Java

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

Kotlin

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

可以在类和方法级别声明 URI 变量，如下例所示：

Java

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

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

|**1**|类级 URI 映射。|
|-----|-------------------------|
|**2**|方法级别的 URI 映射。|

Kotlin

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

    @GetMapping("/pets/{petId}") (2)
    fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
        // ...
    }
}
```

|**1**|类级 URI 映射。|
|-----|-------------------------|
|**2**|方法级别的 URI 映射。|

URI 变量将自动转换为适当的类型，或者生成`TypeMismatchException`。默认情况下支持简单类型（`int`，`long`，`Date`，等等），你可以注册对任何其他数据类型的支持。参见[类型转换](#webflux-ann-typeconversion)和[`DataBinder`]。

URI 变量可以显式地命名（例如，`@PathVariable("customId")`），但是如果名称相同，并且你可以使用调试信息或 Java8 上的`-parameters`编译器标志来编译代码，则可以忽略这些细节。

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

语法`{varName:regex}`声明一个 URI 变量，其正则表达式的语法为：`{varName:regex}`。例如，给定一个`/spring-web-3.0.5.jar`的 URL，下面的方法会提取名称、版本和文件扩展名：

Java

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

Kotlin

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

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

|   |Spring WebFlux 将`PathPattern`和`PathPatternParser`用于 URI 路径匹配支持。<br/>这两个类都位于`spring-web`中，并且明确地设计用于在 Web 应用程序中使用 http url<br/>路径，其中在运行时匹配了大量的 URI 路径模式。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

Spring WebFlux 不支持后缀模式匹配——不像 Spring MVC，其中像`/person`这样的映射也匹配到`/person.*`。对于基于 URL 的内容协商，如果需要，我们建议使用一个查询参数，该参数更简单，更明确，并且不易受到基于 URL 路径的攻击。

##### 模式比较

[Web MVC](web.html#mvc-ann-requestmapping-pattern-comparison)

当多个模式匹配一个 URL 时，必须对它们进行比较以找到最佳匹配。这是用`PathPattern.SPECIFICITY_COMPARATOR`完成的，它寻找更具体的模式。

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

包罗万象的模式（例如，`**`，`{*varName}`）被排除在评分之外，并且总是排在最后。如果两种模式都是包罗万象的，则选择较长的模式。

##### 可消费媒体类型

[Web MVC](web.html#mvc-ann-requestmapping-consumes)

你可以基于请求的`Content-Type`缩小请求映射，如下例所示：

Java

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

Kotlin

```
@PostMapping("/pets", consumes = ["application/json"])
fun addPet(@RequestBody pet: Pet) {
    // ...
}
```

Consumes 属性还支持否定表达式——例如，`!text/plain`表示除`text/plain`以外的任何内容类型。

你可以在类级别声明一个共享的`consumes`属性。然而，与大多数其他请求映射属性不同的是，当在类级使用时，方法级`consumes`属性覆盖而不是扩展类级声明。

|   |`MediaType`提供了常用媒体类型的常量——例如，`APPLICATION_JSON_VALUE`和`APPLICATION_XML_VALUE`。|
|---|--------------------------------------------------------------------------------------------------------------------------------|

##### 可生产媒体类型

[Web MVC](web.html#mvc-ann-requestmapping-produces)

你可以基于`Accept`请求头和控制器方法产生的内容类型列表来缩小请求映射的范围，如下例所示：

Java

```
@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    // ...
}
```

Kotlin

```
@GetMapping("/pets/{petId}", produces = ["application/json"])
@ResponseBody
fun getPet(@PathVariable String petId): Pet {
    // ...
}
```

媒体类型可以指定字符集。支持否定表达式——例如，`!text/plain`表示除`text/plain`以外的任何内容类型。

你可以在类级别声明一个共享的`produces`属性。然而，与大多数其他请求映射属性不同的是，当在类级别使用时，方法级别`produces`属性覆盖而不是扩展类级别声明。

|   |`MediaType`提供了常用媒体类型的常量，例如`APPLICATION_JSON_VALUE`，`APPLICATION_XML_VALUE`。|
|---|---------------------------------------------------------------------------------------------------------------------|

##### 参数和标题

[Web MVC](web.html#mvc-ann-requestmapping-params-and-headers)

你可以根据查询参数条件缩小请求映射的范围。你可以测试查询参数的存在（`myParam`）、它的不存在（`!myParam`）或特定值（`myParam=myValue`）。下面的示例测试具有值的参数：

Java

```
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
```

|**1**|检查`myParam`等于`myValue`。|
|-----|--------------------------------------|

Kotlin

```
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
    // ...
}
```

|**1**|检查`myParam`等于`myValue`。|
|-----|--------------------------------------|

你也可以在请求头条件中使用相同的方法，如下面的示例所示：

Java

```
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
```

|**1**|检查`myHeader`等于`myValue`。|
|-----|---------------------------------------|

Kotlin

```
@GetMapping("/pets", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
    // ...
}
```

|**1**|检查`myHeader`等于`myValue`。|
|-----|---------------------------------------|

##### HTTP 头，选项

[Web MVC](web.html#mvc-ann-requestmapping-head-options)

`@GetMapping`和`@RequestMapping(method=HttpMethod.GET)`透明地支持用于请求映射目的的 HTTP head。控制器的方法不需要改变。在`HttpHandler`服务器适配器中应用的响应包装器确保将`Content-Length`头设置为不实际写入响应的字节数。

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

对于不带 HTTP 方法声明的`@RequestMapping`，`Allow`头被设置为`GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS`。控制器方法应该总是声明受支持的 HTTP 方法（例如，通过使用 HTTP 方法特定的变体—`@GetMapping`，`@PostMapping`，以及其他）。

你可以显式地将`@RequestMapping`方法映射到 HTTPHead 和 HTTPOptions，但在常见情况下这不是必需的。

##### 自定义注释

[Web MVC](web.html#mvc-ann-requestmapping-composed)

Spring WebFlux 支持使用[组合注释](core.html#beans-meta-annotations)进行请求映射。这些注释本身是用`@RequestMapping`进行元注释的，其组成是为了重新声明`@RequestMapping`属性的一个子集（或全部），具有更窄、更具体的目的。

`@GetMapping`，`@PostMapping`，`@PutMapping`，`@DeleteMapping`，和`@PatchMapping`是合成注释的例子。提供它们是因为，可以说，大多数控制器方法应该映射到特定的 HTTP 方法，而不是使用`@RequestMapping`，后者默认情况下与所有 HTTP 方法匹配。如果你需要一个组合注释的示例，请查看这些注释是如何声明的。

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

##### 显式注册

[Web MVC](web.html#mvc-ann-requestmapping-registration)

你可以以编程方式注册处理程序方法，这些方法可以用于动态注册或高级情况，例如同一处理程序在不同 URL 下的不同实例。下面的示例展示了如何做到这一点：

Java

```
@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**|为控制器注入目标处理程序和处理程序映射。|
|-----|---------------------------------------------------------------|
|**2**|准备请求映射元数据。|
|**3**|获取 handler 方法。|
|**4**|添加注册。|

Kotlin

```
@Configuration
class MyConfig {

    @Autowired
    fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)

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

        val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)

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

|**1**|为控制器注入目标处理程序和处理程序映射。|
|-----|---------------------------------------------------------------|
|**2**|准备请求映射元数据。|
|**3**|获取 handler 方法。|
|**4**|添加注册。|

#### 1.4.3.处理程序方法

[Web MVC](web.html#mvc-ann-methods)

`@RequestMapping`处理程序方法具有灵活的签名，并且可以从受支持的控制器方法参数和返回值的范围中进行选择。

##### 方法参数

[Web MVC](web.html#mvc-ann-arguments)

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

对于需要解析阻塞 I/O（例如，读取请求主体）的参数，支持反应性类型（reactor，RxJava，[or other](#webflux-reactive-libraries)）。这一点在“描述”栏中进行了标记。在不需要阻塞的参数上，反应式类型是不被期望的。

JDK1.8 的`java.util.Optional`作为方法参数被支持，并与具有`required`属性（例如，`@RequestParam`，`@RequestHeader`，以及其他）的注释相结合，并且与`required=false`等价。

|                              Controller method argument                               |说明|
|---------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|                                  `ServerWebExchange`                                  |访问完整的`ServerWebExchange`—用于 HTTP 请求和响应的容器、<br/>请求和会话属性、`checkNotModified`方法等。|
|                       `ServerHttpRequest`, `ServerHttpResponse`                       |访问 HTTP 请求或响应。|
|                                     `WebSession`                                      |访问会话。除非添加了属性<br/>，否则不强制开始新的会话。支持反应性类型。|
|                               `java.security.Principal`                               |当前经过身份验证的用户——如果已知的话，可能是特定的<br/>实现类。<br/>支持反应性类型。|
|                         `org.springframework.http.HttpMethod`                         |请求的 HTTP 方法。|
|                                  `java.util.Locale`                                   |当前的请求区域设置，由可用的最特定的`LocaleResolver`确定—在<br/>效果中，配置的`LocaleResolver`/`LocaleContextResolver`。|
|                       `java.util.TimeZone` + `java.time.ZoneId`                       |与当前请求相关联的时区，由`LocaleContextResolver`确定。|
|                                    `@PathVariable`                                    |用于访问 URI 模板变量。见[URI 模式](#webflux-ann-requestmapping-uri-templates)。|
|                                   `@MatrixVariable`                                   |用于访问 URI 路径段中的名称-值对。见[矩阵变量](#webflux-ann-matrix-variables)。|
|                                    `@RequestParam`                                    |用于访问 Servlet 请求参数。参数值被转换为声明的<br/>方法参数类型。参见[`@RequestParam`]。<br/><br/>注意，`@RequestParam`的使用是可选的——例如，用于设置其属性。<br/>参见本表后面的“任何其他参数”。|
|                                   `@RequestHeader`                                    |用于访问请求头。标头值被转换为声明的方法参数<br/>type。参见[`@RequestHeader`]。|
|                                    `@CookieValue`                                     |获取 cookie 的权限。cookie 值被转换为声明的方法参数类型。<br/>参见[`@CookieValue`]。|
|                                    `@RequestBody`                                     |用于访问 HTTP 请求主体。通过使用`HttpMessageReader`实例，主体内容被转换为声明的方法<br/>参数类型。支持反应性类型。<br/>参见[`@RequestBody`]。|
|                                    `HttpEntity<B>`                                    |用于访问请求头和主体。主体使用`HttpMessageReader`实例进行转换。<br/>实例支持反应类型。参见[`HttpEntity`]。|
|                                    `@RequestPart`                                     |用于访问`multipart/form-data`请求中的部件。支持反应类型。<br/>参见[多部分内容](#webflux-multipart-forms)和[多部分数据](#webflux-multipart)。|
|`java.util.Map`, `org.springframework.ui.Model`, and `org.springframework.ui.ModelMap`.|用于访问 HTML 控制器中使用的模型，并作为<br/>视图呈现的一部分暴露于模板中。|
|                                   `@ModelAttribute`                                   |用于访问模型中的现有属性（如果不存在则实例化），并应用<br/>数据绑定和验证。参见[`@ModelAttribute`]（#Webflux-ann-modelattrib-method-args）以及<br/>as[`Model`]（#Webflux-ann-modelattrib-methods）和[<`DataBinder`（#Webflux-ann-initbinder）。<br/><br/>注意，使用<br/>是可选的，例如，用于设置其属性。|
|                               `Errors`, `BindingResult`                               |用于访问来自命令对象的验证和数据绑定的错误，即`@ModelAttribute`参数。一个`Errors`或`BindingResult`参数必须在经过验证的方法参数之后立即声明<br/>。|
|                  `SessionStatus` + class-level `@SessionAttributes`                   |要标记表单处理完成，这将触发清除通过类级`@SessionAttributes`注释声明的会话属性<br/>。<br/>有关更多详细信息，请参见[`@SessionAttributes`]。|
|                                `UriComponentsBuilder`                                 |用于准备相对于当前请求的主机、端口、方案和<br/>上下文路径的 URL。见`@RequestParam`。|
|                                  `@SessionAttribute`                                  |用于访问任何会话属性——与存储在会话<br/>中的模型属性形成对比，后者是类级别`@SessionAttributes`声明的结果。有关更多详细信息，请参见[`@SessionAttribute`]。|
|                                  `@RequestAttribute`                                  |用于访问请求属性。有关更多详细信息，请参见[`@RequestAttribute`]。|
|                                  Any other argument                                   |如果方法参数与上述任何一个参数不匹配，则默认情况下将其解析为<br/>a`@RequestParam`如果它是一个简单类型，则由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)、<br/>或 a`@ModelAttribute`确定，否则。|

##### 返回值

[Web MVC](web.html#mvc-ann-return-types)

下表显示了受支持的控制器方法的返回值。请注意，对于所有返回值，通常都支持来自诸如 reactor、rxjava、[or other](#webflux-reactive-libraries)等库的反应类型。

|                        Controller method return value                        |说明|
|------------------------------------------------------------------------------| |
|                               `@ResponseBody`                                |返回值通过`HttpMessageWriter`实例进行编码，并写入响应。<br/>参见[`@ResponseBody`]。|
|                     `HttpEntity<B>`, `ResponseEntity<B>`                     |返回值指定了完整的响应，包括 HTTP 头，并且通过`HttpMessageWriter`实例对主体进行编码<br/>，并将其写入响应。<br/>参见[`ResponseEntity`]（#WebFlux-Ann-ResponseEntity）。|
|                                `HttpHeaders`                                 |返回带有标题而没有正文的响应。|
|                                   `String`                                   |要用`ViewResolver`实例解析的视图名称，并与隐式<br/>模型一起使用——通过命令对象和`@ModelAttribute`方法确定。处理程序<br/>方法还可以通过声明一个`Model`参数<br/>（描述[earlier](#webflux-viewresolution-handling)）以编程方式丰富模型。|
|                                    `View`                                    |一个`View`实例用于与隐式模型一起进行渲染——通过命令对象和<br/>方法确定<br/>。处理程序方法还可以通过声明一个`Model`参数<br/>（描述[earlier](#webflux-viewresolution-handling)）以编程方式丰富模型。|
|               `java.util.Map`, `org.springframework.ui.Model`                |要添加到隐式模型中的属性，并根据请求路径隐式地确定视图名称<br/>。|
|                              `@ModelAttribute`                               |要添加到模型中的一个属性，其视图名称是基于请求路径隐式确定的<br/>。<br/><br/>注意，[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)是可选的。请参阅下面的<br/>中的“任何其他返回值”。|
|                                 `Rendering`                                  |用于模型和视图呈现场景的 API。|
|                                    `void`                                    |如果方法具有`void`，可能是异步的（例如`Mono<Void>`），返回类型（或`null`返回<br/>值），则认为该方法已完全处理了响应，如果该方法还具有`ServerHttpResponse`、<br/>参数，或`ServerWebExchange`注释。同样也是真的<br/>如果控制器进行了正的 ETag 或`lastModified`时间戳检查。<br/>//todo:详情请参见[控制器](#webflux-caching-etag-lastmodified)。<br/><br/>如果以上都不是真的，`void`返回类型还可以表示<br/>REST 控制器的“无响应体”或 HTML 控制器的默认视图名称选择。|
|`Flux<ServerSentEvent>`, `Observable<ServerSentEvent>`, or other reactive type|发出服务器发送的事件。当只需要<br/>写入数据时（但是，`text/event-stream`必须通过<br/>属性在映射中请求或声明`text/event-stream`），可以省略`ServerSentEvent`包装器。|
|                            Any other return value                            |如果一个返回值与上述任一项不匹配，则默认情况下，它被视为一个视图<br/>名称，如果它是`String`或（应用默认的视图名称选择），或者作为一个要添加到模型中的模型<br/>属性，除非是简单的类型，如由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定的，否则<br/>在这种情况下仍未解决。|

##### 类型转换

[Web MVC](web.html#mvc-ann-typeconversion)

一些表示基于字符串的请求输入的带注释的控制器方法参数（例如，`@RequestParam`，`@RequestHeader`，`@PathVariable`，`@MatrixVariable`，和`@CookieValue`）可以要求类型转换，如果参数被声明为`String`以外的内容。

对于这样的情况，类型转换是基于配置的转换器自动应用的。默认情况下，支持简单类型（如`int`、`long`、`Date`等）。可以通过`WebDataBinder`（参见[`null`]（#WebFlux-Ann-initbinder））或通过使用`FormattingConversionService`注册`Formatters`来定制类型转换（参见[Spring Field Formatting](core.html#format)）。

类型转换中的一个实际问题是空字符串源值的处理。如果由于类型转换而使该值变为[Spring Field Formatting](core.html#format)，则将其视为缺失。这可能是`Long`、`UUID`和其他目标类型的情况。如果要允许注入`null`，可以在参数注释上使用`required`标志，或者将参数声明为`null`。

##### 矩阵变量

[Web MVC](web.html#mvc-ann-matrix-variables)

[RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.3)讨论路径段中的名称-值对。在 Spring WebFlux 中，我们将那些称为基于 Tim Berners-Lee 的[“old post”](https://www.w3.org/DesignIssues/MatrixURIs.html)的“矩阵变量”，但它们也可以称为 URI 路径参数。

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

Spring 与 MVC 不同，在 WebFlux 中，URL 中是否存在矩阵变量并不影响请求映射。换句话说，你不需要使用 URI 变量来屏蔽变量内容。也就是说，如果你想从控制器方法访问矩阵变量，则需要在期望矩阵变量的路径段中添加一个 URI 变量。下面的示例展示了如何做到这一点：

Java

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

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

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

Kotlin

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

@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {

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

鉴于所有的路径段都可以包含矩阵变量，因此有时你可能需要消除矩阵变量预期在哪个路径变量中的歧义，如下例所示：

Java

```
// 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
}
```

Kotlin

```
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
        @MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
        @MatrixVariable(name = "q", pathVar = "petId") q2: Int) {

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

你可以定义一个可以定义为可选的矩阵变量，并指定一个默认值，如下例所示：

Java

```
// GET /pets/42

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

    // q == 1
}
```

Kotlin

```
// GET /pets/42

@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {

    // q == 1
}
```

要获取所有矩阵变量，请使用`MultiValueMap`，如下例所示：

Java

```
// 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]
}
```

Kotlin

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

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

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

##### `@RequestParam`

[Web MVC](web.html#mvc-ann-requestparam)

可以使用`@RequestParam`注释将查询参数绑定到控制器中的方法参数。下面的代码片段显示了该用法：

Java

```
@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`。|
|-----|----------------------|

Kotlin

```
import org.springframework.ui.set

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

    // ...

    @GetMapping
    fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
        val pet = clinic.loadPet(petId)
        model["pet"] = pet
        return "petForm"
    }

    // ...
}
```

|**1**|使用`@RequestParam`。|
|-----|----------------------|

|   |Servlet API“Request Parameter”概念将查询参数、表单<br/>数据和多个部分合并为一个。然而，在 WebFlux 中，每个都可以通过`ServerWebExchange`单独访问。虽然`@RequestParam`仅绑定到查询参数，但你可以使用<br/>数据绑定来将查询参数、表单数据和多个部分应用到`@RequestParam`。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

默认情况下需要使用`@RequestParam`注释的方法参数，但是可以通过将`@RequestParam`的所需标志设置为`false`，或者通过使用`java.util.Optional`包装器声明参数来指定方法参数是可选的。

如果目标方法参数类型不是`String`，则自动应用类型转换。见[类型转换](#webflux-ann-typeconversion)。

当在`Map<String, String>`或`MultiValueMap<String, String>`参数上声明<br/>注释时，映射将填充所有查询参数。

请注意，`@RequestParam`的使用是可选的——例如，用于设置其属性。默认情况下，任何参数是一个简单的值类型（由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定）且不是由任何其他参数解析器解析的，都将被视为用`@RequestParam`进行了注释。

##### `@RequestHeader`

[Web MVC](web.html#mvc-ann-requestheader)

可以使用`@RequestHeader`注释将请求头绑定到控制器中的方法参数。

下面的示例展示了一个带有标题的请求：

```
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-Encoding`和`Keep-Alive`标题的值：

Java

```
@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, (1)
        @RequestHeader("Keep-Alive") long keepAlive) { (2)
    //...
}
```

|**1**|获取`Accept-Encoging`标头的值。|
|-----|----------------------------------------------|
|**2**|获取`Model`标头的值。|

Kotlin

```
@GetMapping("/demo")
fun handle(
        @RequestHeader("Accept-Encoding") encoding: String, (1)
        @RequestHeader("Keep-Alive") keepAlive: Long) { (2)
    //...
}
```

|**1**|获取`Accept-Encoging`标头的值。|
|-----|----------------------------------------------|
|**2**|获取`Keep-Alive`标头的值。|

如果目标方法参数类型不是`String`，则自动应用类型转换。见[类型转换](#webflux-ann-typeconversion)。

当在`@RequestHeader`、`MultiValueMap<String, String>`或`HttpHeaders`参数上使用`@RequestHeader`注释时，映射将填充所有头值。

|   |内置支持用于将逗号分隔的字符串转换为<br/>数组或字符串集合或类型转换系统已知的其他类型。对于<br/>示例，用`@RequestHeader("Accept")`注释的方法参数可以是类型`String`但也可以是类型`String[]`或`List<String>`。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

##### `@CookieValue`

[Web MVC](web.html#mvc-ann-cookievalue)

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

下面的示例显示了一个带有 cookie 的请求：

```
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
```

下面的代码示例演示了如何获得 Cookie 值：

爪哇

```
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
    //...
}
```

|**1**|获取 cookie 值。|
|-----|---------------------|

Kotlin

```
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
    //...
}
```

|**1**|获取 cookie 值。|
|-----|---------------------|

如果目标方法参数类型不是`String`，则自动应用类型转换。见[类型转换](#webflux-ann-typeconversion)。

##### `@ModelAttribute`

[Web MVC](web.html#mvc-ann-modelattrib-method-args)

你可以在方法参数上使用`@ModelAttribute`注释来访问模型中的一个属性，如果不存在，也可以实例化它。model 属性还覆盖了查询参数和表单字段的值，这些字段的名称与字段名称匹配。这被称为数据绑定，它使你不必处理解析和转换单个查询参数和窗体字段的问题。下面的示例绑定`Pet`的实例：

爪哇

```
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
```

|**1**|绑定`Pet`的实例。|
|-----|--------------------------|

Kotlin

```
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } (1)
```

|**1**|绑定`Pet`的实例。|
|-----|--------------------------|

前面示例中的`Pet`实例解析如下：

* 如果已经通过[`Model`]（#Webflux-ann-modelattrib-methods）添加了该模型。

* 从 HTTP 会话到[`@SessionAttributes`]（#WebFlux-Ann-SessionAttributes）。

* 从默认构造函数的调用。

* 调用带有匹配查询参数或表单字段的参数的“主构造函数”。参数名称是通过 爪哇Beans`@ConstructorProperties`或通过字节码中的运行时保留参数名称确定的。

在获得模型属性实例之后，再进行数据绑定。`WebExchangeDataBinder`类将查询参数和窗体字段的名称与目标`Object`上的字段名称匹配。在必要时应用类型转换后，将填充匹配字段。有关数据绑定（和验证）的更多信息，请参见[验证](core.html#validation)。有关自定义数据绑定的更多信息，请参见[`DataBinder`]。

数据绑定可能会导致错误。默认情况下，会引发`WebExchangeBindException`，但是，要检查控制器方法中的此类错误，可以在`BindingResult`旁边立即添加一个`BindingResult`参数，如下例所示：

爪哇

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

|**1**|添加`BindingResult`。|
|-----|-------------------------|

Kotlin

```
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
    if (result.hasErrors()) {
        return "petForm"
    }
    // ...
}
```

|**1**|添加`BindingResult`。|
|-----|-------------------------|

通过添加`javax.validation.Valid`注释或 Spring 的`@Validated`注释，可以在数据绑定后自动应用验证（另请参见[Bean Validation](core.html#validation-beanvalidation)和[Spring validation](core.html#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`。|
|-----|---------------------------------------------|

Kotlin

```
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
    if (result.hasErrors()) {
        return "petForm"
    }
    // ...
}
```

|**1**|在模型属性参数上使用`@Valid`。|
|-----|---------------------------------------------|

Spring 与 Spring MVC 不同，WebFlux 在模型中支持反应性类型——例如，`Mono<Account>`或`io.reactivex.Single<Account>`。你可以声明一个`@ModelAttribute`参数，带或不带反应性类型包装器，如果有必要，它将相应地解析为实际值。但是，请注意，要使用`BindingResult`参数，你必须在不使用反应式类型包装器的情况下声明`@ModelAttribute`参数，如前面所示。或者，你也可以通过 reactive 类型来处理任何错误，如下例所示：

爪哇

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

Kotlin

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

请注意，`@ModelAttribute`的使用是可选的——例如，用于设置其属性。默认情况下，任何不是简单值类型（由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定）且未由任何其他参数解析器解析的参数都将被视为已用`@ModelAttribute`注释。

##### `@SessionAttributes`

[Web MVC](web.html#mvc-ann-sessionattributes)

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

考虑以下示例：

爪哇

```
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
    // ...
}
```

|**1**|使用`@SessionAttributes`注释。|
|-----|------------------------------------------|

Kotlin

```
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
    // ...
}
```

|**1**|使用`@SessionAttributes`注释。|
|-----|------------------------------------------|

在第一个请求中，当将名称为`pet`的 model 属性添加到 model 时，它会自动升级到`WebSession`并保存在`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**|使用`@SessionAttributes`注释。|
|-----|------------------------------------------|
|**2**|使用`SessionStatus`变量。|

Kotlin

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

    // ...

    @PostMapping("/pets/{id}")
    fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String { (2)
        if (errors.hasErrors()) {
            // ...
        }
        status.setComplete()
        // ...
    }
}
```

|**1**|使用`@SessionAttributes`注释。|
|-----|------------------------------------------|
|**2**|使用`SessionStatus`变量。|

##### `@SessionAttribute`

[Web MVC](web.html#mvc-ann-sessionattribute)

如果你需要访问已存在的会话属性，这些属性是全局管理的（也就是说，在控制器之外——例如，由过滤器管理），并且可能存在，也可能不存在，那么你可以在方法参数上使用`@SessionAttribute`注释，如下例所示：

爪哇

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

|**1**|使用`SessionStatus`。|
|-----|--------------------------|

Kotlin

```
@GetMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
    // ...
}
```

|**1**|使用`@SessionAttribute`。|
|-----|--------------------------|

对于需要添加或删除会话属性的用例，可以考虑将`WebSession`注入到控制器方法中。

对于将会话中的模型属性临时存储为控制器工作流的一部分，可以考虑使用`SessionAttributes`，如[`@SessionAttributes`]中所述。

##### `@RequestAttribute`

[Web MVC](web.html#mvc-ann-requestattrib)

与`@SessionAttribute`类似，你可以使用`@RequestAttribute`注释来访问先前创建的预先存在的请求属性（例如，通过`WebFilter`），如下例所示：

爪哇

```
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
    // ...
}
```

|**1**|使用`WebFilter`。|
|-----|--------------------------|

Kotlin

```
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
    // ...
}
```

|**1**|使用`@RequestAttribute`。|
|-----|--------------------------|

##### 多部分内容

[Web MVC](web.html#mvc-multipart-forms)

正如[多部分数据](#webflux-multipart)中所解释的，`ServerWebExchange`提供了对多部分内容的访问。在控制器中处理文件上载表单（例如，从浏览器）的最佳方法是通过数据绑定到[命令对象](#webflux-ann-modelattrib-method-args)，如下例所示：

爪哇

```
class MyForm {

    private String name;

    private MultipartFile file;

    // ...

}

@Controller
public class FileUploadController {

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

}
```

Kotlin

```
class MyForm(
        val name: String,
        val file: MultipartFile)

@Controller
class FileUploadController {

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

}
```

你还可以在 RESTful 服务场景中提交来自非浏览器客户端的多部分请求。下面的示例与 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`获取文件。|

Kotlin

```
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, (1)
        @RequestPart("file-data") FilePart file): String { (2)
    // ...
}
```

|**1**|使用`@RequestPart`获取元数据。|
|-----|-----------------------------------------|
|**2**|使用`@RequestPart`获取文件。|

要反序列化 RAW Part 内容（例如，到 JSON——类似于`@RequestBody`），你可以声明一个具体的目标`Object`，而不是[多部分数据](#webflux-multipart)，如下例所示：

爪哇

```
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
    // ...
}
```

|**1**|使用`@RequestPart`获取元数据。|
|-----|-----------------------------------------|

Kotlin

```
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
    // ...
}
```

|**1**|使用`@RequestPart`获取元数据。|
|-----|-----------------------------------------|

你可以将`@RequestPart`与`javax.validation.Valid`或 Spring 的`@Validated`注释结合使用，这将导致应用标准 Bean 验证。验证错误会导致`WebExchangeBindException`，从而导致 400（bad\_request）响应。异常包含带有错误详细信息的`BindingResult`，也可以通过使用异步包装器声明参数并使用与错误相关的操作符来在 Controller 方法中进行处理：

爪哇

```
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
    // use one of the onError* operators...
}
```

Kotlin

```
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
    // ...
}
```

要以`MultiValueMap`的形式访问所有多部分数据，可以使用`@RequestBody`，如下例所示：

爪哇

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

|**1**|使用`@RequestBody`。|
|-----|---------------------|

Kotlin

```
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
    // ...
}
```

|**1**|使用`@RequestMapping`。|
|-----|---------------------|

要按顺序访问多部分数据，在流式方式中，你可以使用`@RequestBody`与`Flux<Part>`（或`Flow<Part>`在 Kotlin 中）代替，如下例所示：

爪哇

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

|**1**|使用`@RequestBody`。|
|-----|---------------------|

Kotlin

```
@PostMapping("/")
fun handle(@RequestBody parts: Flow<Part>): String { (1)
    // ...
}
```

|**1**|使用`@RequestBody`。|
|-----|---------------------|

##### `@RequestBody`

[Web MVC](web.html#mvc-ann-requestbody)

你可以使用`@RequestBody`注释，通过[HttpMessageReader](#webflux-codecs)将请求主体读取并反序列化为`Object`。下面的示例使用了`@RequestBody`参数：

爪哇

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

Kotlin

```
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
    // ...
}
```

与 Spring MVC 不同，在 WebFlux 中，`@RequestBody`方法参数支持反应性类型和完全非阻塞的读取和（客户机到服务器）流。

爪哇

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

Kotlin

```
@PostMapping("/accounts")
fun handle(@RequestBody accounts: Flow<Account>) {
    // ...
}
```

你可以使用[WebFlux 配置](#webflux-config)的[HTTP 消息编解码器](#webflux-config-message-codecs)选项来配置或自定义消息阅读器。

你可以将`@RequestBody`与`javax.validation.Valid`或 Spring 的[Web MVC](web.html#mvc-ann-modelattrib-method-args)注释结合使用，这将导致应用标准 Bean 验证。验证错误会导致`WebExchangeBindException`，从而导致 400（bad\_request）响应。异常包含带有错误详细信息的`BindingResult`，可以通过使用异步包装器声明参数，然后使用与错误相关的操作符，在 Controller 方法中进行处理：

爪哇

```
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Mono<Account> account) {
    // use one of the onError* operators...
}
```

Kotlin

```
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Mono<Account>) {
    // ...
}
```

##### `HttpEntity`

[Web MVC](web.html#mvc-ann-httpentity)

`HttpEntity`与使用[`@RequestBody`]（#WebFlux-Ann-RequestBody）或多或少相同，但它基于一个容器对象，该对象公开了请求头和主体。下面的示例使用`HttpEntity`:

爪哇

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

Kotlin

```
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
    // ...
}
```

##### `@ResponseBody`

[Web MVC](web.html#mvc-ann-responsebody)

你可以在方法上使用`@ResponseBody`注释，通过[HttpMessageWriter](#webflux-codecs)将返回序列化到响应主体。下面的示例展示了如何做到这一点：

爪哇

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

Kotlin

```
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
    // ...
}
```

`@ResponseBody`在类级别上也受到支持，在这种情况下，所有控制器方法都会继承它。这是`@RestController`的效果，它不过是一个标记有`@Controller`和`@ResponseBody`的元注释。

`@ResponseBody`支持反应类型，这意味着你可以返回 reactor 或 rxjava 类型，并将它们产生的异步值呈现给响应。有关更多详细信息，请参见[Streaming](#webflux-codecs-streaming)和[JSON 渲染](#webflux-codecs-jackson)。

你可以将`@ResponseBody`方法与 JSON 序列化视图合并。详见[JacksonJSON](#webflux-ann-jackson)。

可以使用[WebFlux 配置](#webflux-config)的[HTTP 消息编解码器](#webflux-config-message-codecs)选项来配置或自定义消息写入。

##### `ResponseEntity`

[Web MVC](web.html#mvc-ann-responseentity)

`ResponseEntity`类似于[`@ResponseBody`]（#WebFlux-Ann-ResponseBody），但具有状态和标题。例如：

爪哇

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

Kotlin

```
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
    val body: String = ...
    val etag: String = ...
    return ResponseEntity.ok().eTag(etag).build(body)
}
```

WebFlux 支持使用单个值[反应型](#webflux-reactive-libraries)异步地生成`ResponseEntity`，和/或为主体生成单个值和多值的反应类型。这允许使用`ResponseEntity`的各种异步响应，如下所示：

* `ResponseEntity<Mono<T>>`或`ResponseEntity<Flux<T>>`在稍后异步提供主体时，立即使响应状态和头为已知。如果主体由 0.1 个值组成，则使用`Mono`；如果可以产生多个值，则使用`Flux`。

* `Mono<ResponseEntity<T>>`在稍后的时间点异步提供了所有这三个方面——响应状态、头和主体。这允许响应状态和头根据异步请求处理的结果而变化。

* `Mono<ResponseEntity<Mono<T>>>`或`Mono<ResponseEntity<Flux<T>>>`是另一种可能的选择，尽管不太常见。它们首先异步地提供响应状态和报头，然后是响应主体，也是异步地提供响应主体。

##### JacksonJSON

Spring 提供对 JacksonJSON 库的支持。

##### JSON 视图 #

[Web MVC](web.html#mvc-ann-jackson)

Spring WebFlux 提供了对[Jackson 的序列化视图](https://www.baeldung.com/jackson-json-view-annotation)的内置支持，它只允许呈现`Object`中所有字段的一个子集。要与`@ResponseBody`或`@ResponseBody`控制器方法一起使用它，你可以使用 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;
    }
}
```

Kotlin

```
@RestController
class UserController {

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

class User(
        @JsonView(WithoutPasswordView::class) val username: String,
        @JsonView(WithPasswordView::class) val password: String
) {
    interface WithoutPasswordView
    interface WithPasswordView : WithoutPasswordView
}
```

|   |<br/>允许一个视图类的数组，但是每个<br/>控制器方法只能指定一个。如果需要激活多个视图，请使用复合接口。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.4.4.`Model`

[Web MVC](web.html#mvc-ann-modelattrib-methods)

你可以使用`@ModelAttribute`注释：

* 在[方法参数](#webflux-ann-modelattrib-method-args)中的`@RequestMapping`方法上创建或访问模型中的对象，并通过`WebDataBinder`将其绑定到请求。

* 作为`@Controller`或`@ModelAttribute`类中的方法级注释，在任何`@RequestMapping`方法调用之前帮助初始化模型。

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

本节讨论`@ModelAttribute`方法，或者前面列表中的第二个项。控制器可以有任意数量的`@ModelAttribute`方法。所有这些方法都是在同一个控制器中的`@RequestMapping`方法之前调用的。还可以通过`@ControllerAdvice`在控制器之间共享`@ModelAttribute`方法。有关更多详细信息，请参见[财务总监建议](#webflux-ann-controller-advice)一节。

`@ModelAttribute`方法具有灵活的方法签名。它们支持许多与`@RequestMapping`方法相同的参数（除了`@ModelAttribute`本身和与请求主体相关的任何参数）。

下面的示例使用`@ModelAttribute`方法：

爪哇

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

Kotlin

```
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
    model.addAttribute(accountRepository.findAccount(number))
    // add more ...
}
```

下面的示例只添加了一个属性：

爪哇

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

Kotlin

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

|   |当未显式指定名称时，默认的名称是基于类型选择的，<br/>，正如在 爪哇doc 中对[`Conventions`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/core/conventions.html）所解释的那样。<br/>通过重载的`addAttribute`方法或`addAttribute`方法或<br/>（用于返回值）上的 name 属性，始终可以指定一个显式名称。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

Spring WebFlux 与 Spring MVC 不同，在模型中显式地支持反应类型（例如，或）。这样的异步模型属性可以在`@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) {
    // ...
}
```

Kotlin

```
import org.springframework.ui.set

@ModelAttribute
fun addAccount(@RequestParam number: String) {
    val accountMono: Mono<Account> = accountRepository.findAccount(number)
    model["account"] = accountMono
}

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

此外，在视图呈现之前，具有反应性类型包装器的任何模型属性都将被解析为它们的实际值（以及模型更新）。

你还可以使用`@ModelAttribute`作为`@RequestMapping`方法的方法级注释，在这种情况下，`@RequestMapping`方法的返回值被解释为一个模型属性。这通常不是必需的，因为这是 HTML 控制器中的默认行为，除非返回值是`String`，否则该返回值将被解释为视图名称。`@RequestBody`还可以帮助自定义模型属性名，如下例所示：

爪哇

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

Kotlin

```
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
    // ...
    return account
}
```

#### 1.4.5.`DataBinder`

[Web MVC](web.html#mvc-ann-initbinder)

`@Controller`或`@ControllerAdvice`类可以具有`@InitBinder`方法，以初始化`WebDataBinder`的实例。这些反过来又被用来：

* 将请求参数（即表单数据或查询）绑定到模型对象。

* 将基于`String`的请求值（例如请求参数、路径变量、头、cookie 和其他）转换为控制器方法参数的目标类型。

* 在呈现 HTML 窗体时，将模型对象值格式化为`String`值。

`@InitBinder`方法可以注册控制器特定的`java.beans.PropertyEditor`或 Spring `Converter`和`Formatter`组件。此外，可以使用[WebFlux 爪哇 配置](#webflux-config-conversion)在全局共享的`FormattingConversionService`中注册`Converter`和`Formatter`类型。

`@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**|使用`@InitBinder`注释。|
|-----|-----------------------------------|

Kotlin

```
@Controller
class FormController {

    @InitBinder (1)
    fun initBinder(binder: WebDataBinder) {
        val dateFormat = SimpleDateFormat("yyyy-MM-dd")
        dateFormat.isLenient = false
        binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
    }

    // ...
}
```

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

爪哇

```
@Controller
public class FormController {

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

    // ...
}
```

|**1**|添加自定义格式化程序（在本例中为`DateFormatter`）。|
|-----|------------------------------------------------------------|

Kotlin

```
@Controller
class FormController {

    @InitBinder
    fun initBinder(binder: WebDataBinder) {
        binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) (1)
    }

    // ...
}
```

|**1**|添加自定义格式化程序（在本例中为`DateFormatter`）。|
|-----|------------------------------------------------------------|

#### 1.4.6.管理异常

[Web MVC](web.html#mvc-ann-exceptionhandler)

`@Controller`和[@controlleradvice](#webflux-ann-controller-advice)类可以使用`@ExceptionHandler`方法来处理来自控制器方法的异常。以下示例包括这样的处理程序方法：

爪哇

```
@Controller
public class SimpleController {

    // ...

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

|**1**|声明`@ExceptionHandler`。|
|-----|---------------------------------|

Kotlin

```
@Controller
class SimpleController {

    // ...

    @ExceptionHandler (1)
    fun handle(ex: IOException): ResponseEntity<String> {
        // ...
    }
}
```

|**1**|声明`@ExceptionHandler`。|
|-----|---------------------------------|

异常可以与正在传播的顶级异常（即抛出的直接`IOException`）匹配，也可以与顶级包装异常中的直接原因匹配（例如，在`IOException`中包装的`IllegalStateException`）。

为了匹配异常类型，最好将目标异常声明为方法参数，如前面的示例所示。或者，注释声明可以缩小异常类型以进行匹配。我们通常建议在参数签名中尽可能具体，并在`@ControllerAdvice`上以相应的顺序优先声明主根异常映射。详见[MVC 部门](web.html#mvc-ann-exceptionhandler)。

|   |WebFlux 中的`@ExceptionHandler`方法支持与<br/>方法相同的方法参数和<br/>返回值，但请求主体-<br/>和`@ModelAttribute`-相关的方法参数除外。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

Spring WebFlux 中对`@ExceptionHandler`方法的支持是由`HandlerAdapter`方法提供的。有关更多详细信息，请参见[`DispatcherHandler`]。

##### REST API 异常

[Web MVC](web.html#mvc-ann-rest-exceptions)

REST 服务的一个常见要求是在响应主体中包含错误详细信息。 Spring 框架不会自动这样做，因为响应主体中的错误细节的表示是特定于应用程序的。但是，`@RestController`可以使用带有`ResponseEntity`返回值的`@ExceptionHandler`方法来设置响应的状态和主体。这样的方法也可以在`@ControllerAdvice`类中声明，以在全局范围内应用它们。

|   |请注意， Spring WebFlux 对于 Spring MVC没有一个等价的，因为 WebFlux 仅引发（或其子类），并且这些不需要被翻译为一个 HTTP 状态代码。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.4.7.财务总监建议

[Web MVC](web.html#mvc-ann-controller-advice)

通常，`@ExceptionHandler`、`@InitBinder`和`@ModelAttribute`方法应用于声明它们的`@Controller`类（或类层次结构）中。如果你希望这样的方法在全局范围内（在控制器之间）应用得更多，那么可以在一个用`@ControllerAdvice`或`@RestControllerAdvice`注释的类中声明它们。

`@ControllerAdvice`注释为`@Component`，这意味着这样的类可以通过[组件扫描](core.html#beans-java-instantiating-container-scan)注册为 Spring bean。`@RestControllerAdvice`是一种组合注释，它同时使用`@ControllerAdvice`和`@ResponseBody`进行注释，其本质上意味着`@ExceptionHandler`方法通过消息转换（与视图解析或模板呈现）呈现到响应主体。

在启动时，`@RequestMapping`和`@ExceptionHandler`方法的基础设施类检测用`@ControllerAdvice`注释的 Spring bean，然后在运行时应用它们的方法。全局`@ExceptionHandler`方法（来自`@ControllerAdvice`）应用于*之后*局部方法（来自`@Controller`）。相比之下，全局`@ModelAttribute`和`@InitBinder`方法应用于局部方法*在此之前*。

默认情况下，`@ControllerAdvice`方法适用于每个请求（即所有控制器），但你可以通过使用注释上的属性将其缩小到控制器的子集，如下例所示：

爪哇

```
// 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 {}
```

Kotlin

```
// 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 {}
```

前面示例中的选择器是在运行时进行评估的，如果广泛使用，可能会对性能产生负面影响。有关更多详细信息，请参见[`@ControllerAdvice`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/bind/annotation/controlleradvice.html）爪哇doc。

### 1.5.功能端点

[Web MVC](web.html#webmvc-fn)

Spring WebFlux 包括 WebFlux.FN，这是一种轻量级函数式编程模型，其中函数被用于路由和处理请求，并且契约被设计为具有不可变性。它是基于注释的编程模型的一种替代方案，但在其他情况下运行在相同的[反应核](#webflux-reactive-spring-web)基础上。

#### 1.5.1.概述

[Web MVC](web.html#webmvc-fn-overview)

在 WebFlux.FN 中，一个 HTTP 请求是用`HandlerFunction`处理的：一个函数接受`ServerRequest`并返回一个延迟的`ServerResponse`（即`Mono<ServerResponse>`）。请求和响应对象都具有不可更改的契约，这些契约提供对 HTTP 请求和响应的 JDK8 友好访问。`HandlerFunction`相当于基于注释的编程模型中的`@RequestMapping`方法的主体。

传入的请求被路由到带有`RouterFunction`的处理程序函数：该函数接受`ServerRequest`并返回延迟的`HandlerFunction`（即`Mono<HandlerFunction>`）。当路由器函数匹配时，将返回一个处理程序函数；否则将返回一个空的 mono。`RouterFunction`相当于`@RequestMapping`注释，但与此的主要区别是，路由器函数不仅提供数据，还提供行为。

`RouterFunctions.route()`提供了一个路由器构建器，可以促进路由器的创建，如下例所示：

爪哇

```
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) {
        // ...
    }
}
```

Kotlin

```
val repository: PersonRepository = ...
val handler = PersonHandler(repository)

val route = coRouter { (1)
    accept(APPLICATION_JSON).nest {
        GET("/person/{id}", handler::getPerson)
        GET("/person", handler::listPeople)
    }
    POST("/person", handler::createPerson)
}

class PersonHandler(private val repository: PersonRepository) {

    // ...

    suspend fun listPeople(request: ServerRequest): ServerResponse {
        // ...
    }

    suspend fun createPerson(request: ServerRequest): ServerResponse {
        // ...
    }

    suspend fun getPerson(request: ServerRequest): ServerResponse {
        // ...
    }
}
```

|**1**|使用协程路由器 DSL 创建路由器，还可以通过`router { }`提供反应式替代方案。|
|-----|-----------------------------------------------------------------------------------------------------|

运行`RouterFunction`的一种方法是将其转换为`HttpHandler`，并通过一个内置的[服务器适配器](#webflux-httphandler)安装它：

* `RouterFunctions.toHttpHandler(RouterFunction)`

* `RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)`

大多数应用程序都可以通过 WebFlux 爪哇 配置运行，参见[运行服务器](#webflux-fn-running)。

#### 1.5.2.handlerfunction

[Web MVC](web.html#webmvc-fn-handler-functions)

`ServerRequest`和`ServerResponse`是不可变的接口，它们提供对 HTTP 请求和响应的 JDK8 友好访问。请求和响应都针对体流提供[反应流](https://www.reactive-streams.org)反压。请求主体用反应器`Flux`或`Mono`表示。响应体用任何反应流`Publisher`表示，包括`Flux`和`Mono`。有关该问题的更多信息，请参见[反应库](#webflux-reactive-libraries)。

##### ServerRequest

`ServerRequest`提供对 HTTP 方法、URI、标头和查询参数的访问，而对正文的访问是通过`body`方法提供的。

下面的示例将请求主体提取到`Mono<String>`:

爪哇

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

Kotlin

```
val string = request.awaitBody<String>()
```

下面的示例将主体提取到`Flux<Person>`（或 Kotlin 中的`Flow<Person>`），其中`Person`对象是从形式化的形式（例如 JSON 或 XML）中解码的：

爪哇

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

Kotlin

```
val people = request.bodyToFlow<Person>()
```

前面的示例是使用更通用的`ServerRequest.body(BodyExtractor)`的快捷方式，它接受`BodyExtractor`功能策略接口。实用程序类`BodyExtractors`提供了对许多实例的访问。例如，前面的示例也可以写如下：

爪哇

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

Kotlin

```
    val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle()
    val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()
```

下面的示例展示了如何访问表单数据：

爪哇

```
Mono<MultiValueMap<String, String>> map = request.formData();
```

Kotlin

```
val map = request.awaitFormData()
```

下面的示例展示了如何以地图的形式访问多部分数据：

爪哇

```
Mono<MultiValueMap<String, Part>> map = request.multipartData();
```

Kotlin

```
val map = request.awaitMultipartData()
```

下面的示例展示了如何以流媒体方式一次访问多个部分：

爪哇

```
Flux<Part> parts = request.body(BodyExtractors.toParts());
```

Kotlin

```
val parts = request.body(BodyExtractors.toParts()).asFlow()
```

##### ServerResponse

`ServerResponse`提供对 HTTP 响应的访问，由于它是不可变的，你可以使用`build`方法来创建它。你可以使用构建器设置响应状态、添加响应头或提供主体。下面的示例使用 JSON 内容创建一个 200（OK）响应：

爪哇

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

Kotlin

```
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)
```

下面的示例展示了如何使用`Location`标头构建 201（已创建）响应，而不使用正文：

爪哇

```
URI location = ...
ServerResponse.created(location).build();
```

Kotlin

```
val location: URI = ...
ServerResponse.created(location).build()
```

根据使用的编解码器，可以传递提示参数来自定义如何序列化或反序列化主体。例如，要指定[JacksonJSON 视图](https://www.baeldung.com/jackson-json-view-annotation):

爪哇

```
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
```

Kotlin

```
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
```

##### 处理程序类

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

爪哇

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

Kotlin

```
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }
```

这很方便，但在一个应用程序中，我们需要多个功能，而多个内联 lambda 可能会变得混乱。因此，将相关的处理程序函数组合成一个处理程序类是有用的，该处理程序类在基于注释的应用程序中具有与`@Controller`类似的作用。例如，下面的类公开了一个反应性`Person`存储库：

爪哇

```
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

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).bodyValue(person))
            .switchIfEmpty(ServerResponse.notFound().build());
    }
}
```

|**1**|`listPeople`是一个处理函数，它将存储库中找到的所有`Person`对象作为<br/>json 返回。|
|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|`createPerson`是一个处理函数，它存储了一个包含在请求主体中的新`Person`。<br/>注意，`PersonRepository.savePerson(Person)`返回`Mono<Void>`:一个空的`Mono`，当该人已从请求中读取并存储时，它会发出<br/>完成信号。因此，我们使用`build(Publisher<Void>)`方法在接收到完成信号时（即<br/>保存了`Person`时）发送响应。|
|**3**|`getPerson`是一个处理函数，它返回一个人，由`id`路径<br/>变量标识。我们从存储库中检索`Person`并创建一个 JSON 响应，如果找到了<br/>。如果没有找到它，我们使用`switchIfEmpty(Mono<T>)`返回 404Not Found 响应。|

Kotlin

```
class PersonHandler(private val repository: PersonRepository) {

    suspend fun listPeople(request: ServerRequest): ServerResponse { (1)
        val people: Flow<Person> = repository.allPeople()
        return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
    }

    suspend fun createPerson(request: ServerRequest): ServerResponse { (2)
        val person = request.awaitBody<Person>()
        repository.savePerson(person)
        return ok().buildAndAwait()
    }

    suspend fun getPerson(request: ServerRequest): ServerResponse { (3)
        val personId = request.pathVariable("id").toInt()
        return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
                ?: ServerResponse.notFound().buildAndAwait()

    }
}
```

|**1**|`listPeople`是一个处理函数，它将存储库中找到的所有`Person`对象作为<br/>json 返回。|
|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|`createPerson`是一个处理函数，它存储了一个包含在请求主体中的新`Person`。<br/>注意，`PersonRepository.savePerson(Person)`是一个没有返回类型的挂起函数。|
|**3**|`getPerson`是一个处理函数，它返回一个人，由`id`路径<br/>变量标识。我们从存储库中检索`Person`并创建一个 JSON 响应，如果找到了<br/>。如果没有找到它，我们将返回 404Not Found 响应。|

##### Validation

功能端点可以使用 Spring 的[验证设施](core.html#validation)将验证应用于请求主体。例如，给定用于`Person`的自定义 Spring [Validator](core.html#validation)实现：

爪哇

```
public class PersonHandler {

    private final Validator validator = new PersonValidator(); (1)

    // ...

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

    private void validate(Person person) {
        Errors errors = new BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw new ServerWebInputException(errors.toString()); (3)
        }
    }
}
```

|**1**|创建`Validator`实例。|
|-----|-----------------------------------|
|**2**|应用验证。|
|**3**|提出 400 响应的例外情况。|

Kotlin

```
class PersonHandler(private val repository: PersonRepository) {

    private val validator = PersonValidator() (1)

    // ...

    suspend fun createPerson(request: ServerRequest): ServerResponse {
        val person = request.awaitBody<Person>()
        validate(person) (2)
        repository.savePerson(person)
        return ok().buildAndAwait()
    }

    private fun validate(person: Person) {
        val errors: Errors = BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw ServerWebInputException(errors.toString()) (3)
        }
    }
}
```

|**1**|创建`Validator`实例。|
|-----|-----------------------------------|
|**2**|应用验证。|
|**3**|提出 400 响应的例外情况。|

处理程序还可以通过基于`LocalValidatorFactoryBean`创建和注入一个全局`Validator`实例来使用标准 Bean 验证 API（JSR-303）。见[Spring Validation](core.html#validation-beanvalidation)。

#### 1.5.3.`RouterFunction`

[Web MVC](web.html#webmvc-fn-router-functions)

路由器函数用于将请求路由到相应的`HandlerFunction`。通常，你不会自己编写路由器函数，而是使用`RouterFunctions`实用程序类上的一个方法来创建一个。`RouterFunctions.route()`（无参数）为你提供了用于创建路由器函数的 Fluent 构建器，而`RouterFunctions.route(RequestPredicate, HandlerFunction)`提供了一种直接创建路由器的方法。

通常，建议使用`route()`Builder，因为它为典型的映射场景提供了方便的快捷方式，而不需要很难发现的静态导入。例如，Router Function Builder 提供了方法`GET(String, HandlerFunction)`来创建 GET 请求的映射；以及`POST(String, HandlerFunction)`用于 POST。

除了基于 HTTP 方法的映射，Route Builder 还提供了一种在映射到请求时引入额外谓词的方法。对于每个 HTTP 方法，都有一个重载变量，该变量将`RequestPredicate`作为参数，尽管可以表示该参数的附加约束。

##### 谓词

你可以编写自己的`RequestPredicate`，但是`RequestPredicates`实用程序类提供了基于请求路径、HTTP 方法、Content-type 等的常用实现。下面的示例使用一个请求谓词来基于`Accept`头创建一个约束：

爪哇

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

Kotlin

```
val route = coRouter {
    GET("/hello-world", accept(TEXT_PLAIN)) {
        ServerResponse.ok().bodyValueAndAwait("Hello World")
    }
}
```

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

* `RequestPredicate.and(RequestPredicate)`—两者必须匹配。

* `RequestPredicate.or(RequestPredicate)`—两者都可以匹配。

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

##### 路线

对路由器的功能按顺序进行评估：如果第一条路由不匹配，则对第二条路由进行评估，依此类推。因此，在一般路线之前声明更具体的路线是有意义的。当将路由器功能注册为 Spring bean 时，这一点也很重要，后面将对此进行说明。请注意，这种行为与基于注释的编程模型不同，在该模型中，“最特定的”控制器方法是自动选择的。

当使用 Router 函数 builder 时，所有定义的路由都被组合成一个`RouterFunction`，从`build()`返回。还有其他方法可以将多个路由器功能组合在一起：

* `add(RouterFunction)`上的`RouterFunctions.route()`构建器

* `RouterFunction.and(RouterFunction)`

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

下面的示例显示了四条路线的组成：

爪哇

```
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 匹配的`Accept`标头的`GET /person/{id}`被路由到`PersonHandler.getPerson`|
|-----|--------------------------------------------------------------------------------------------------|
|**2**|带有与 JSON 匹配的`Accept`头的`GET /person`被路由到`PersonHandler.listPeople`|
|**3**|没有附加谓词的`POST /person`映射到`PersonHandler.createPerson`，并且|
|**4**|`otherRoute`是在其他地方创建的路由器功能，并将其添加到构建的路由中。|

Kotlin

```
import org.springframework.http.MediaType.APPLICATION_JSON

val repository: PersonRepository = ...
val handler = PersonHandler(repository);

val otherRoute: RouterFunction<ServerResponse> = coRouter {  }

val route = coRouter {
    GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
    GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
    POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
```

|**1**|带有与 JSON 匹配的`GET /person/{id}`头的`Accept`被路由到`PersonHandler.getPerson`|
|-----|--------------------------------------------------------------------------------------------------|
|**2**|带有与 JSON 匹配的`GET /person`头的`Accept`被路由到`PersonHandler.listPeople`|
|**3**|没有附加谓词的`POST /person`映射到`PersonHandler.createPerson`，并且|
|**4**|`otherRoute`是在其他地方创建的路由器功能，并将其添加到构建的路由中。|

##### 嵌套路线

一组路由器函数通常有一个共享谓词，例如共享路径。在上面的示例中，共享谓词将是一个匹配`/person`的路径谓词，由三个路由使用。在使用注释时，可以使用映射到`/person`的类型级`@RequestMapping`注释来删除这种重复。在 WebFlux.FN 中，路径谓词可以通过 Router Function Builder 上的`path`方法共享。例如，通过使用嵌套路由，可以通过以下方式改进上面示例的最后几行：

爪哇

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

|**1**|请注意，`path`的第二个参数是接受路由器生成器的使用者。|
|-----|-----------------------------------------------------------------------------------|

Kotlin

```
val route = coRouter {
    "/person".nest {
        GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        GET(accept(APPLICATION_JSON), handler::listPeople)
        POST("/person", handler::createPerson)
    }
}
```

尽管基于路径的嵌套是最常见的，但你可以通过在 Builder 上使用`nest`方法在任何类型的谓词上进行嵌套。上面仍然包含一些以共享`Accept`-header 谓词形式出现的重复。我们可以通过使用`nest`方法和`accept`方法来进一步改进：

爪哇

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

Kotlin

```
val route = coRouter {
    "/person".nest {
        accept(APPLICATION_JSON).nest {
            GET("/{id}", handler::getPerson)
            GET(handler::listPeople)
            POST("/person", handler::createPerson)
        }
    }
}
```

#### 1.5.4.运行服务器

[Web MVC](web.html#webmvc-fn-running)

如何在 HTTP 服务器中运行路由器功能？一个简单的选择是使用以下方法之一将路由器函数转换为`HttpHandler`:

* `RouterFunctions.toHttpHandler(RouterFunction)`

* `RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)`

然后，你可以将返回的`HttpHandler`与多个服务器适配器一起使用，方法是按照[Httphandler](#webflux-httphandler)执行特定于服务器的指令。

一个更典型的选项（也被 Spring boot 使用）是通过[WebFlux 配置](#webflux-config)使用基于[`DispatcherHandler`]（#WebFlux-Dispatcher-Handler）的设置运行，该设置使用 Spring 配置来声明处理请求所需的组件。WebFlux 爪哇 配置声明了以下支持功能端点的基础设施组件：

* `RouterFunctionMapping`:在 Spring 配置中检测一个或多个`RouterFunction<?>`bean，[命令他们](core.html#beans-factory-ordered)，通过`RouterFunction.andOther`对它们进行组合，并将请求路由到结果组合的`RouterFunction`。

* `HandlerFunctionAdapter`:允许`DispatcherHandler`调用映射到请求的`HandlerFunction`的简单适配器。

* `ServerResponseResultHandler`:通过调用`ServerResponse`的`writeTo`方法来处理调用`HandlerFunction`的结果。

前面的组件让功能端点适合`DispatcherHandler`请求处理生命周期，并且（可能）与带注释的控制器（如果声明了任何控制器的话）并排运行。这也是 Spring 引导 WebFlux 启动器启用功能端点的方式。

下面的示例显示了一个 WebFlux 爪哇 配置（有关如何运行它，请参见[DispatcherHandler](#webflux-dispatcher-handler)）：

爪哇

```
@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...
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    @Bean
    fun routerFunctionA(): RouterFunction<*> {
        // ...
    }

    @Bean
    fun routerFunctionB(): RouterFunction<*> {
        // ...
    }

    // ...

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        // configure message conversion...
    }

    override fun addCorsMappings(registry: CorsRegistry) {
        // configure CORS...
    }

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // configure view resolution for HTML rendering...
    }
}
```

#### 1.5.5.过滤处理程序函数

[Web MVC](web.html#webmvc-fn-handler-filter-function)

你可以使用路由函数生成器上的`before`、`after`或`filter`方法来过滤处理程序函数。对于注释，你可以通过使用`@ControllerAdvice`、`ServletFilter`或同时使用这两种方法来实现类似的功能。筛选器将应用于由构建器构建的所有路由。这意味着嵌套路由中定义的筛选器不适用于“顶层”路由。例如，考虑以下示例：

爪哇

```
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**|添加自定义请求头的`before`过滤器仅应用于两个 GET 路由。|
|-----|----------------------------------------------------------------------------------------------|
|**2**|记录响应的`after`过滤器应用于所有路由，包括嵌套的路由。|

Kotlin

```
val route = router {
    "/person".nest {
        GET("/{id}", handler::getPerson)
        GET("", handler::listPeople)
        before { (1)
            ServerRequest.from(it)
                    .header("X-RequestHeader", "Value").build()
        }
        POST("/person", handler::createPerson)
        after { _, response -> (2)
            logResponse(response)
        }
    }
}
```

|**1**|添加自定义请求头的`before`过滤器仅应用于两个 GET 路由。|
|-----|----------------------------------------------------------------------------------------------|
|**2**|记录响应的`after`过滤器应用于所有路由，包括嵌套的路由。|

路由器构建器上的`filter`方法接受`HandlerFilterFunction`:一个函数接受`ServerRequest`和`HandlerFunction`并返回`ServerResponse`。处理程序函数参数表示链中的下一个元素。这通常是路由到的处理程序，但是如果应用了多个，它也可以是另一个过滤器。

现在，我们可以在路由中添加一个简单的安全过滤器，假设我们有一个`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();
```

Kotlin

```
val securityManager: SecurityManager = ...

val route = router {
        ("/person" and accept(APPLICATION_JSON)).nest {
            GET("/{id}", handler::getPerson)
            GET("", handler::listPeople)
            POST("/person", handler::createPerson)
            filter { request, next ->
                if (securityManager.allowAccessTo(request.path())) {
                    next(request)
                }
                else {
                    status(UNAUTHORIZED).build();
                }
            }
        }
    }
```

前面的示例演示了调用`next.handle(ServerRequest)`是可选的。我们只允许在允许访问的情况下运行处理程序函数。

除了在路由器功能构建器上使用`filter`方法外，还可以通过`RouterFunction.filter(HandlerFilterFunction)`对现有的路由器功能应用过滤器。

|   |CORS 对功能端点的支持是通过专用的[`CorsWebFilter`]提供的。|
|---|---------------------------------------------------------------------------------------------------------------------------------|

### 1.6.URI 链接

[Web MVC](web.html#mvc-uri-building)

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

#### 1.6.1.尿酸成分

Spring MVC 和 Spring WebFlux

`UriComponentsBuilder`有助于从具有变量的 URI 模板构建 URI，如下例所示：

爪哇

```
UriComponents uriComponents = UriComponentsBuilder
        .fromUriString("https://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`。|

Kotlin

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

val uri = uriComponents.expand("Westin", "123").toUri()  (5)
```

|**1**|带有 URI 模板的静态工厂方法。|
|-----|-----------------------------------------------------------|
|**2**|添加或替换 URI 组件。|
|**3**|请求对 URI 模板和 URI 变量进行编码。|
|**4**|构建`UriComponents`。|
|**5**|展开变量并获得`URI`。|

前面的示例可以合并为一个链，并用`buildAndExpand`将其缩短，如下例所示：

爪哇

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

Kotlin

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

你可以通过直接访问一个 URI（这意味着编码）来进一步缩短它，如下例所示：

爪哇

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

Kotlin

```
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123")
```

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

爪哇

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

Kotlin

```
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123")
```

#### 1.6.2.UriBuilder

Spring MVC 和 Spring WebFlux

[`UriComponentsBuilder`]实现`UriBuilder`。你可以创建`UriBuilder`，然后使用`UriBuilderFactory`。同时，`UriBuilderFactory`和`UriBuilder`提供了一种基于共享配置（例如基本 URL、编码首选项和其他细节）的可插入机制，用于从 URI 模板构建 URI。

你可以使用`UriBuilderFactory`配置`RestTemplate`和`WebClient`来定制 URI 的准备。`DefaultUriBuilderFactory`是`UriBuilderFactory`的默认实现，它在内部使用`UriComponentsBuilder`并公开共享配置选项。

下面的示例展示了如何配置`RestTemplate`:

Java

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

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

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

Kotlin

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

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

val restTemplate = RestTemplate()
restTemplate.uriTemplateHandler = factory
```

下面的示例配置`WebClient`:

Java

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

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

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

Kotlin

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

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

val client = WebClient.builder().uriBuilderFactory(factory).build()
```

此外，还可以直接使用`DefaultUriBuilderFactory`。它类似于使用`UriComponentsBuilder`，但它是一个实际的实例，它保存配置和首选项，而不是静态工厂方法，如下例所示：

Java

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

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

Kotlin

```
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)

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

#### 1.6.3.URI 编码

Spring MVC 和 Spring WebFlux

`UriComponentsBuilder`在两个级别上公开编码选项：

* [uricomponentsbuilder#encode（）](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/util/UriComponentsBuilder.html#encode--):先对 URI 模板进行预编码，然后在展开时对 URI 变量进行严格编码。

* [uricomponents#encode（）](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/util/UriComponents.html#encode--):编码 URI 组件*之后*URI 变量被展开。

这两个选项都用转义的八进制替换非 ASCII 和非法字符。然而，第一个选项也用 URI 变量中出现的保留意义替换字符。

|   |考虑一下“；”，它在某种程度上是合法的，但具有保留的含义。第一个选项在 URI 变量中用“%3b”替换<br/>；；"，但不在 URI 模板中。相比之下，第二个选项永远不会<br/>取代“；”，因为它是路径中的法律字符。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

在大多数情况下，第一个选项可能会给出预期的结果，因为它将 URI 变量视为不透明的数据来进行完全编码，而如果 URI 变量故意包含保留字符，则第二个选项是有用的。当完全不展开 URI 变量时，第二个选项也很有用，因为这也会对任何看起来像 URI 变量的内容进行编码。

下面的示例使用了第一个选项：

Java

```
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"
```

Kotlin

```
val 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（这意味着编码）来缩短前面的示例，如下例所示：

Java

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

Kotlin

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

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

Java

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

Kotlin

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

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

Java

```
String baseUrl = "https://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();
```

Kotlin

```
val baseUrl = "https://example.com"
val factory = DefaultUriBuilderFactory(baseUrl).apply {
    encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}

// Customize the RestTemplate..
val restTemplate = RestTemplate().apply {
    uriTemplateHandler = factory
}

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

`DefaultUriBuilderFactory`实现在内部使用`UriComponentsBuilder`来扩展和编码 URI 模板。作为工厂，它提供了一个单独的位置来配置编码方法，该方法基于以下编码模式之一：

* `TEMPLATE_AND_VALUES`:使用`UriComponentsBuilder#encode()`（对应于前面列表中的第一个选项）对 URI 模板进行预编码，并在展开时对 URI 变量进行严格编码。

* `VALUES_ONLY`:不对 URI 模板进行编码，而是在将 URI 变量扩展到模板之前，通过`UriUtils#encodeUriVariables`对 URI 变量进行严格编码。

* `URI_COMPONENT`:使用`UriComponents#encode()`，对应于前面列表中的第二个选项，来对 URI 组件值的编码*之后*URI 变量进行扩展。

* `NONE`:不应用编码。

由于历史原因和向后兼容，`RestTemplate`被设置为`EncodingMode.URI_COMPONENT`。`WebClient`依赖于`DefaultUriBuilderFactory`中的默认值，该默认值从 5.0.x 中的`EncodingMode.URI_COMPONENT`更改为 5.1 中的`EncodingMode.TEMPLATE_AND_VALUES`。

### 1.7.科尔斯

[Web MVC](web.html#mvc-cors)

Spring WebFlux 允许你处理 CORS（跨源资源共享）。这一节描述了如何做到这一点。

#### 1.7.1.导言

[Web MVC](web.html#mvc-cors-intro)

出于安全原因，浏览器禁止对当前来源以外的资源进行 Ajax 调用。例如，你可以在一个标签中设置你的银行帐户，而在另一个标签中设置 Evil.com。来自 Evil.com 的脚本不应该能够使用你的凭据向你的银行 API 发出 Ajax 请求——例如，从你的帐户中取款！

跨源资源共享是由[大多数浏览器](https://caniuse.com/#feat=cors)实现的[W3C 规范](https://www.w3.org/TR/cors/)，它允许你指定授权哪种类型的跨域请求，而不是使用基于 iframe 或 JSONP 的安全性较低、功能较弱的解决方案。

#### 1.7.2.处理

[Web MVC](web.html#mvc-cors-processing)

CORS 规范区分了飞行前、简单和实际请求。要了解 CORS 的工作原理，你可以阅读[这篇文章](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)等，或者查看规范以获得更多详细信息。

Spring WebFlux`HandlerMapping`实现为 CORS 提供了内置支持。在成功地将一个请求映射到一个处理程序之后，`HandlerMapping`检查 CORS 配置中给定的请求和处理程序，并采取进一步的操作。前置请求是直接处理的，而简单和实际的 CORS 请求是截获、验证的，并设置了所需的 CORS 响应头。

为了启用跨源请求（即存在`Origin`头并与请求的主机不同），你需要有一些显式声明的 CORS 配置。如果没有找到匹配的 CORS 配置，则拒绝预航前请求。没有 CORS 头被添加到简单的和实际的 CORS 请求的响应中，因此，浏览器会拒绝它们。

每个`HandlerMapping`都可以单独使用基于 URL 模式的[configured](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-)映射`CorsConfiguration`。在大多数情况下，应用程序使用 WebFlux Java 配置来声明这样的映射，这将导致一个单一的全局映射传递给所有`HandlerMapping`实现。

你可以将`HandlerMapping`级别的全局 CORS 配置与更细粒度的、处理程序级别的 CORS 配置结合起来。例如，带注释的控制器可以使用类或方法级别的`@CrossOrigin`注释（其他处理程序可以实现`CorsConfigurationSource`）。

结合全局和局部配置的规则通常是累加的——例如，所有全局配置和所有局部配置。对于那些只能接受单个值的属性，例如`allowCredentials`和`maxAge`，本地重写全局值。详见[`CorsConfiguration#combine(CorsConfiguration)`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/cors/corsconfiguration.html#combine-org.springframework.web.cors.corsconfiguration-）。

|   |要从源代码中了解更多信息或进行高级定制，请参见：<br/><br/>*`CorsConfiguration`<br/><br/>*`AbstractHandlerMapping`和<br/><br/>|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.7.3.`@CrossOrigin`

[Web MVC](web.html#mvc-cors-controller)

[`@CrossOrigin`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/bind/annotation/crossorigin.html）注解可以在带注释的控制器方法上实现跨源请求，如下例所示：

爪哇

```
@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) {
        // ...
    }
}
```

Kotlin

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

    @CrossOrigin
    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}
```

默认情况下，`@CrossOrigin`允许：

* 所有的起源。

* 所有标题。

* 将控制器方法映射到的所有 HTTP 方法。

`allowCredentials`默认情况下不启用，因为这建立了一个信任级别，该级别公开敏感的特定于用户的信息（例如 Cookie 和 CSRF 令牌），并且只应在适当的情况下使用。当启用`allowOrigins`时，要么必须将`allowOrigins`设置为一个或多个特定域（但不是特定值`"*"`），要么可选择将`allowOriginPatterns`属性用于匹配到源集的动态。

`maxAge`设置为 30 分钟。

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

爪哇

```
@CrossOrigin(origins = "https://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) {
        // ...
    }
}
```

Kotlin

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

    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}
```

可以在类和方法级别上使用`@CrossOrigin`，如下例所示：

爪哇

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

    @CrossOrigin("https://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`。|

Kotlin

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

    @CrossOrigin("https://domain2.com") (2)
    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}
```

|**1**|在类级别上使用`@CrossOrigin`。|
|-----|-----------------------------------------|
|**2**|在方法级别使用`@CrossOrigin`。|

#### 1.7.4.全局配置

[Web MVC](web.html#mvc-cors-global)

除了细粒度的控制器方法级配置外，你可能还需要定义一些全局 CORS 配置。你可以在任何`HandlerMapping`上单独设置基于 URL 的`CorsConfiguration`映射。然而，大多数应用程序都使用 WebFlux 爪哇 配置来实现这一点。

默认情况下，全局配置启用以下功能：

* 所有的起源。

* 所有标题。

* `GET`，`HEAD`，和`POST`方法。

`allowedCredentials`默认情况下不启用，因为这建立了一个信任级别，该级别公开敏感的特定于用户的信息（例如 Cookie 和 CSRF 令牌），并且只应在适当的情况下使用。当启用`allowOrigins`时，要么必须将`allowOrigins`设置为一个或多个特定的域（但不是特殊值`"*"`），要么将`allowOriginPatterns`属性用于匹配到源集的动态。

`maxAge`设置为 30 分钟。

要在 WebFlux 爪哇 配置中启用 CORS，可以使用`CorsRegistry`回调，如下例所示：

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

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

        // Add more mappings...
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addCorsMappings(registry: CorsRegistry) {

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

        // Add more mappings...
    }
}
```

#### 1.7.5.CORS`WebFilter`

[Web MVC](web.html#mvc-cors-filter)

你可以通过内置的[`CorsWebFilter`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/cors/active/corswebfilter.html）应用 CORS 支持，这与[功能端点](#webflux-fn)很好地匹配。

|   |如果你试图将`CorsFilter`与 Spring 安全性一起使用，请记住，对于 CORS， Spring <br/>安全性具有[内置支持](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#cors)。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

要配置过滤器，你可以声明一个`CorsWebFilter` Bean，并将一个`CorsConfigurationSource`传递给它的构造函数，如下例所示：

爪哇

```
@Bean
CorsWebFilter corsFilter() {

    CorsConfiguration config = new CorsConfiguration();

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

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

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

    return new CorsWebFilter(source);
}
```

Kotlin

```
@Bean
fun corsFilter(): CorsWebFilter {

    val config = CorsConfiguration()

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

    config.allowCredentials = true
    config.addAllowedOrigin("https://domain1.com")
    config.addAllowedHeader("*")
    config.addAllowedMethod("*")

    val source = UrlBasedCorsConfigurationSource().apply {
        registerCorsConfiguration("/**", config)
    }
    return CorsWebFilter(source)
}
```

### 1.8.网络安全

[Web MVC](web.html#mvc-web-security)

[Spring Security](https://projects.spring.io/spring-security/)项目提供了保护 Web 应用程序免受恶意攻击的支持。请参阅 Spring 安全参考文档，包括：

* [WebFlux 安全性](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#jc-webflux)

* [WebFlux 测试支持](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#test-webflux)

* [CSRF 保护](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#csrf)

* [安全响应标头](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#headers)

### 1.9.查看技术

[Web MVC](web.html#mvc-view)

Spring WebFlux 中对视图技术的使用是可插入的。是否决定使用 ThymeLeaf、FreeMarker 或其他一些视图技术主要是配置更改的问题。本章介绍与 Spring WebFlux 集成的视图技术。我们假设你已经熟悉[视图分辨率](#webflux-viewresolution)。

#### 1.9.1.百里香叶

[Web MVC](web.html#mvc-view-thymeleaf)

ThymeLeaf 是一个现代的服务器端 爪哇 模板引擎，强调自然的 HTML 模板，可以通过双击在浏览器中预览，这对于在 UI 模板上独立工作（例如，由设计师）非常有帮助，而不需要运行的服务器。Thymeleaf 提供了一套广泛的功能，并且它是积极开发和维护的。有关更完整的介绍，请参见[Thymeleaf](https://www.thymeleaf.org/)项目主页。

ThymeLeaf 与 Spring WebFlux 的集成由 ThymeLeaf 项目管理。配置涉及一些 Bean 声明，例如`SpringResourceTemplateResolver`、`SpringWebFluxTemplateEngine`和`ThymeleafReactiveViewResolver`。有关更多详细信息，请参见[Thymeleaf+Spring](https://www.thymeleaf.org/documentation.html)和 WebFlux 集成[公告](http://forum.thymeleaf.org/Thymeleaf-3-0-8-JUST-PUBLISHED-td4030687.html)。

#### 1.9.2.自由标记

[Web MVC](web.html#mvc-view-freemarker)

[Apache Freemarker](https://freemarker.apache.org/)是一个模板引擎，用于生成从 HTML 到电子邮件等任何类型的文本输出。 Spring 框架具有用于使用 Spring WebFlux 和 Freemarker 模板的内置集成。

##### 视图配置

[Web MVC](web.html#mvc-view-freemarker-contextconfig)

下面的示例展示了如何将 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/freemarker");
        return configurer;
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()
    }

    // Configure FreeMarker...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("classpath:/templates/freemarker")
    }
}
```

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

##### 自由标记配置

[Web MVC](web.html#mvc-views-freemarker)

通过在`FreeMarkerConfigurer` Bean 上设置适当的 Bean 属性，可以将自由标记’settings’和’sharedvariables’直接传递给自由标记`Configuration`对象（由 Spring 管理）。`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;
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    // ...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("classpath:/templates")
        setFreemarkerVariables(mapOf("xml_escape" to XmlEscape()))
    }
}
```

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

##### 表单处理

[Web MVC](web.html#mvc-view-freemarker-forms)

Spring 提供了用于 JSP 的标记库，该标记库包括`<spring:bind/>`元素。这个元素主要允许表单显示来自表单支持对象的值，并显示来自 Web 或业务层中`Validator`的失败验证的结果。 Spring 在 Freemarker 中还具有对相同功能的支持，具有用于生成表单输入元素本身的附加方便宏。

##### BIND 宏 #

[Web MVC](web.html#mvc-view-bind-macros)

在`spring-webflux.jar`freemarker 文件中维护了一组标准的宏，因此对于适当配置的应用程序，它们总是可用的。

Spring 模板库中定义的一些宏被认为是内部的（私有的），但是在宏定义中不存在这样的范围，这使得所有的宏对于调用代码和用户模板都是可见的。下面的部分只关注你需要从模板中直接调用的宏。如果你希望直接查看宏代码，那么该文件被称为`spring.ftl`，并且位于`org.springframework.web.reactive.result.view.freemarker`包中。

有关绑定支持的更多详细信息，请参见[简单绑定](web.html#mvc-view-simple-binding)中的 Spring MVC。

##### 表格宏 #

有关 Spring 对自由标记模板的表单宏支持的详细信息，请参阅 Spring MVC 文档的以下部分。

* [输入宏](web.html#mvc-views-form-macros)

* [输入字段](web.html#mvc-views-form-macros-input)

* [选择字段](web.html#mvc-views-form-macros-select)

* [HTML 转义](web.html#mvc-views-form-macros-html-escaping)

#### 1.9.3.脚本视图

[Web MVC](web.html#mvc-view-script)

Spring 框架有一个内置的集成，用于使用 Spring WebFlux 和任何模板库，这些模板库可以在[JSR-223](https://www.jcp.org/en/jsr/detail?id=223)爪哇 脚本引擎之上运行。下表显示了我们在不同的脚本引擎上测试过的模板库：

|脚本库|                  Scripting Engine                   |
|----------------------------------------------------------------------------------|-----------------------------------------------------|
|[Handlebars](https://handlebarsjs.com/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)|
|[Mustache](https://mustache.github.io/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)|
|[React](https://facebook.github.io/react/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)|
|[EJS](https://www.embeddedjs.com/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)|
|[ERB](https://www.stuartellis.name/articles/erb/)|           [JRuby](https://www.jruby.org)            |
|[字符串模板](https://docs.python.org/2/library/string.html#template-strings)|          [Jython](https://www.jython.org/)          |
|[Kotlin Script templating](https://github.com/sdeleuze/kotlin-script-templating)|          [Kotlin](https://kotlinlang.org/)          |

|   |集成任何其他脚本引擎的基本规则是，它必须实现`ScriptEngine`和`Invocable`接口。|
|---|------------------------------------------------------------------------------------------------------------------------------|

##### 所需经费

[Web MVC](web.html#mvc-view-script-dependencies)

你需要在 Classpath 上安装脚本引擎，其细节因脚本引擎而异：

* [Nashorn](https://openjdk.java.net/projects/nashorn/)爪哇Script 引擎由 爪哇8+ 提供。强烈推荐使用最新的可用更新版本。

* [JRuby](https://www.jruby.org)应该作为 Ruby 支持的依赖项添加。

* [Jython](https://www.jython.org)应该作为 Python 支持的依赖项添加。

* 对于 Kotlin 脚本支持，应该添加`org.jetbrains.kotlin:kotlin-script-util`依赖项和包含`META-INF/services/javax.script.ScriptEngineFactory`行的`org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory`文件。有关更多详细信息，请参见[这个例子](https://github.com/sdeleuze/kotlin-script-templating)。

你需要有脚本模板库。实现 爪哇Script 的一种方法是通过[WebJars](https://www.webjars.org/)。

##### 脚本模板

[Web MVC](web.html#mvc-view-script-integrate)

你可以声明一个`ScriptTemplateConfigurer` Bean 来指定要使用的脚本引擎、要加载的脚本文件、调用什么函数来呈现模板，等等。下面的示例使用了 Mustache 模板和 Nashorn 爪哇Script 引擎：

爪哇

```
@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;
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.scriptTemplate()
    }

    @Bean
    fun configurer() = ScriptTemplateConfigurer().apply {
        engineName = "nashorn"
        setScripts("mustache.js")
        renderObject = "Mustache"
        renderFunction = "render"
    }
}
```

使用以下参数调用`render`函数：

* `String template`:模板内容

* `Map model`:视图模型

* `RenderingContext renderingContext`:[`RenderingContext`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/view/script/renderingcontext.html），它提供了对应用程序上下文、区域设置、模板装入器和 URL（自 5.0 起）的访问权限

`Mustache.render()`与该签名在本机上兼容，因此你可以直接调用它。

如果模板技术需要进行一些定制，那么可以提供一个实现定制呈现功能的脚本。例如，[Handlerbars](https://handlebarsjs.com)在使用模板之前需要对其进行编译，并且需要[polyfill](https://en.wikipedia.org/wiki/Polyfill)，以便模拟服务器端脚本引擎中不可用的一些浏览器功能。下面的示例展示了如何设置自定义呈现函数：

爪哇

```
@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("polyfill.js", "handlebars.js", "render.js");
        configurer.setRenderFunction("render");
        configurer.setSharedEngine(false);
        return configurer;
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.scriptTemplate()
    }

    @Bean
    fun configurer() = ScriptTemplateConfigurer().apply {
        engineName = "nashorn"
        setScripts("polyfill.js", "handlebars.js", "render.js")
        renderFunction = "render"
        isSharedEngine = false
    }
}
```

|   |当使用非线程安全的<br/>脚本引擎时，需要将`sharedEngine`属性设置为`false`，该脚本引擎的模板库不是为并发而设计的，例如在 Nashorn 上运行的手柄或<br/>React。在那种情况下，由于[this bug](https://bugs.openjdk.java.net/browse/JDK-8076099)，爪哇 SE8Update60 是必需的，但是一般情况下<br/>推荐在任何情况下使用最近发布的 爪哇 SE 补丁。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

`polyfill.js`只定义了处理栏正常运行所需的`window`对象，如以下代码片段所示：

```
var window = {};
```

这个基本的`render.js`实现在使用模板之前对其进行编译。为生产准备好的实现还应该存储和重用缓存的模板或预编译的模板。这可以在脚本端完成，也可以在你需要的任何定制中完成（例如，管理模板引擎配置）。下面的示例展示了如何编译模板：

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

查看 Spring Framework Unit 测试，[爪哇](https://github.com/spring-projects/spring-framework/tree/main/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script)和[resources](https://github.com/spring-projects/spring-framework/tree/main/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script)，以获得更多配置示例。

#### 1.9.4.JSON 和 XML

[Web MVC](web.html#mvc-view-jackson)

出于[内容协商](#webflux-multiple-representations)的目的，可以根据客户机请求的内容类型，在使用 HTML 模板或其他格式（例如 JSON 或 XML）呈现模型之间进行替换，这是非常有用的。为了支持这样做， Spring WebFlux 提供了`HttpMessageWriterView`，你可以使用它来插入`spring-web`中的任何可用的[Codecs](#webflux-codecs)，例如`Jackson2JsonEncoder`、`Jackson2SmileEncoder`或`Jaxb2XmlEncoder`。

与其他视图技术不同，`HttpMessageWriterView`不需要`ViewResolver`，而是将[configured](#webflux-config-view-resolvers)作为默认视图。你可以配置一个或多个这样的默认视图，包装不同的`HttpMessageWriter`实例或`Encoder`实例。在运行时使用与请求的内容类型匹配的内容类型。

在大多数情况下，一个模型包含多个属性。要确定要序列化哪个，你可以配置`HttpMessageWriterView`，并使用要用于呈现的 model 属性的名称。如果模型只包含一个属性，则使用该属性。

### 1.10.HTTP 缓存

[Web MVC](web.html#mvc-caching)

HTTP 缓存可以显著提高 Web 应用程序的性能。HTTP 缓存围绕`Cache-Control`响应头和后续的条件请求头，例如`Last-Modified`和`ETag`。`Cache-Control`建议私有（例如，浏览器）和公共（例如，代理）缓存如何缓存和重用响应。`ETag`报头用于发出条件请求，如果内容没有更改，则该请求可能在没有正文的情况下导致 304（未 \_modified）。`ETag`可以看作是`Last-Modified`页眉的更复杂的继承者。

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

#### 1.10.1.`CacheControl`

[Web MVC](web.html#mvc-caching-cachecontrol)

[`CacheControl`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/http/cachecontrol.html）提供了对配置`Cache-Control`标头相关设置的支持，并在许多地方被接受为参数：

* [控制器](#webflux-caching-etag-lastmodified)

* [静态资源](#webflux-caching-static-resources)

虽然[RFC 7234](https://tools.ietf.org/html/rfc7234#section-5.2.2)描述了`Cache-Control`响应头的所有可能的指令，但`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();
```

Kotlin

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

// Prevent caching - "Cache-Control: no-store"
val 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"
val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()
```

#### 1.10.2.控制器

[Web MVC](web.html#mvc-caching-etag-lastmodified)

控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做，因为资源的`lastModified`或`ETag`值需要在与条件请求头进行比较之前进行计算。控制器可以将`ETag`和`Cache-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);
}
```

Kotlin

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

    val book = findBook(id)
    val version = book.getVersion()

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

如果与条件请求标题的比较表明内容没有更改，则前面的示例发送带有空主体的 304（未 \_modified）响应。否则，将`ETag`和`Cache-Control`标头添加到响应中。

你还可以检查控制器中的条件请求头，如下例所示：

爪哇

```
@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（未修改）。没有进一步的处理。|
|**3**|继续处理请求。|

Kotlin

```
@RequestMapping
fun myHandleMethod(exchange: ServerWebExchange, model: Model): String? {

    val eTag: Long = ... (1)

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

    model.addAttribute(...) (3)
    return "myViewName"
}
```

|**1**|应用程序特定的计算。|
|-----|--------------------------------------------------------------------|
|**2**|响应已设置为 304（未修改）。没有进一步的处理。|
|**3**|继续处理请求。|

针对`eTag`值、`lastModified`值或两者检查条件请求有三种变体。对于条件`GET`和`HEAD`请求，可以将响应设置为 304（不是 \_modified）。对于条件`POST`、`PUT`和`DELETE`，可以将响应设置为 412（前提条件 \_ 失败），以防止并发修改。

#### 1.10.3.静态资源

[Web MVC](web.html#mvc-caching-static-resources)

为了获得最佳性能，你应该使用`Cache-Control`和条件响应头来服务静态资源。参见关于配置[静态资源](#webflux-config-static-resources)的部分。

### 1.11.WebFlux 配置

[Web MVC](web.html#mvc-config)

WebFlux 爪哇 配置声明了用带注释的控制器或功能端点处理请求所需的组件，并提供了一个 API 来定制配置。这意味着你不需要理解由 爪哇 配置创建的底层 bean。但是，如果你想了解它们，可以在`WebFluxConfigurationSupport`中看到它们，或者在[Special Bean Types](#webflux-special-bean-types)中阅读有关它们的更多信息。

对于配置 API 中没有的更高级的定制，你可以通过[高级配置模式](#webflux-config-advanced-java)获得对配置的完全控制。

#### 1.11.1.启用 WebFlux 配置

[Web MVC](web.html#mvc-config-enable)

你可以在 爪哇 配置中使用`@EnableWebFlux`注释，如下例所示：

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig {
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig
```

前面的示例注册了许多 Spring WebFlux[基础设施 bean](#webflux-special-bean-types)，并适应了 Classpath 上可用的依赖关系——对于 JSON、XML 和其他的依赖关系。

#### 1.11.2.WebFlux 配置 API

[Web MVC](web.html#mvc-config-customize)

在你的 爪哇 配置中，你可以实现`WebFluxConfigurer`接口，如下例所示：

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    // Implement configuration methods...
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    // Implement configuration methods...
}
```

#### 1.11.3.转换、格式化

[Web MVC](web.html#mvc-config-conversion)

默认情况下，安装了用于各种数字和日期类型的格式化程序，并支持在字段上通过`@NumberFormat`和`@DateTimeFormat`进行定制。

要在 爪哇 Config 中注册自定义格式化程序和转换器，请使用以下方法：

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

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

}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        // ...
    }
}
```

Spring 默认情况下，WebFlux 在解析和格式化日期值时会考虑请求区域设置。这适用于将日期表示为带有“输入”窗体字段的字符串的窗体。但是，对于“日期”和“时间”表单字段，浏览器使用 HTML 规范中定义的固定格式。对于这种情况，日期和时间格式可以定制如下：

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        val registrar = DateTimeFormatterRegistrar()
        registrar.setUseIsoFormat(true)
        registrar.registerFormatters(registry)
    }
}
```

|   |参见[`FormatterRegistrar`SPI]和`FormattingConversionServiceFactoryBean`有关何时<br/>使用`FormatterRegistrar`实现的更多信息。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.11.4.验证

[Web MVC](web.html#mvc-config-validation)

默认情况下，如果[Bean Validation](core.html#validation-beanvalidation-overview)存在于 Classpath（例如， Hibernate 验证器）上，则`LocalValidatorFactoryBean`注册为全局[validator](core.html#validator)，用于`@Valid`和`@Validated`上的`@Controller`方法参数。

在你的 爪哇 配置中，你可以自定义全局`Validator`实例，如下例所示：

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

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

}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun getValidator(): Validator {
        // ...
    }

}
```

请注意，你也可以在本地注册`Validator`实现，如下例所示：

爪哇

```
@Controller
public class MyController {

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

}
```

Kotlin

```
@Controller
class MyController {

    @InitBinder
    protected fun initBinder(binder: WebDataBinder) {
        binder.addValidators(FooValidator())
    }
}
```

|   |如果需要在某个地方注入一个`LocalValidatorFactoryBean`，请创建一个 Bean 并将<br/>标记为`@Primary`，以避免与 MVC 配置中声明的那个冲突。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.11.5.内容类型解析器

[Web MVC](web.html#mvc-config-content-negotiation)

你可以配置 Spring WebFlux 如何从请求中确定`@Controller`实例所请求的媒体类型。默认情况下，只检查`Accept`头，但你也可以启用基于查询参数的策略。

下面的示例展示了如何自定义所请求的内容类型分辨率：

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

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

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureContentTypeResolver(builder: RequestedContentTypeResolverBuilder) {
        // ...
    }
}
```

#### 1.11.6.HTTP 消息编解码器

[Web MVC](web.html#mvc-config-message-converters)

下面的示例展示了如何自定义如何读取和写入请求和响应主体：

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().maxInMemorySize(512 * 1024);
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        // ...
    }
}
```

`ServerCodecConfigurer`提供了一组默认的读取器和写入器。你可以使用它来添加更多的读取器和编写器，自定义缺省的读取器和编写器，或者完全替换缺省的读取器和编写器。

对于 Jackson 的 JSON 和 XML，可以考虑使用[`Jackson2ObjectMapperBuilder`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/http/converter/json/Jackson2objectmapperbuilder.html），它使用以下属性定制 Jackson 的默认属性：

* [`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`]（https://fasterxml.github.io/Jackson-databind/javadoc/2.6/com/fasterxml/Jackson/databind/deserializationfeature.html#fail_on_unknown_properties）被禁用。

* [`MapperFeature.DEFAULT_VIEW_INCLUSION`]（https://fasterxml.github.io/Jackson-databind/javadoc/2.6/com/fasterxml/Jackson/databind/mapperfeature.html#default_view_inclusion）被禁用。

如果在 Classpath 上检测到以下已知模块，它还会自动注册这些模块：

* [`jackson-datatype-joda`]（https://github.com/fasterxml/Jackson-datatype-joda）：支持 joda-time 类型。

* [`jackson-datatype-jsr310`]（https://github.com/fasterxml/Jackson-datatype-jsr310）：支持 爪哇8 日期和时间 API 类型。

* [`jackson-datatype-jdk8`]（https://github.com/fasterxml/Jackson-datatype-jdk8）：支持其他 爪哇8 类型，例如`Optional`。

* [`jackson-module-kotlin`]（https://github.com/fasterxml/Jackson-module- Kotlin）：支持 Kotlin 类和数据类。

#### 1.11.7.视图解析器

[Web MVC](web.html#mvc-config-view-resolvers)

下面的示例展示了如何配置视图分辨率：

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

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

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // ...
    }
}
```

`ViewResolverRegistry`具有 Spring 框架与之集成的视图技术的快捷方式。下面的示例使用 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;
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()
    }

    // Configure Freemarker...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("classpath:/templates")
    }
}
```

你还可以插入任何`ViewResolver`实现，如下例所示：

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

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

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        val resolver: ViewResolver = ...
        registry.viewResolver(resolver
    }
}
```

为了支持[内容协商](#webflux-multiple-representations)和通过视图分辨率呈现其他格式（除了 HTML），你可以基于`HttpMessageWriterView`实现配置一个或多个默认视图，该实现接受来自`spring-web`的任何可用的[Codecs](#webflux-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));
    }

    // ...
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()

        val encoder = Jackson2JsonEncoder()
        registry.defaultViews(HttpMessageWriterView(encoder))
    }

    // ...
}
```

有关集成 Spring WebFlux 的视图技术的更多信息，请参见[查看技术](#webflux-view)。

#### 1.11.8.静态资源

[Web MVC](web.html#mvc-config-static-resources)

此选项提供了一种方便的方式，可以从[`Resource`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/io/resource.html）-based 位置的列表中提供静态资源。

在下一个示例中，给定一个以`/resources`开头的请求，相对路径用于在 Classpath 上查找和服务相对于`/static`的静态资源。资源将在一年后到期，以确保最大程度地使用浏览器缓存并减少浏览器发出的 HTTP 请求。还计算`Last-Modified`头，如果存在，则返回`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));
    }

}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public", "classpath:/static/")
                .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
    }
}
```

资源处理程序还支持[`ResourceResolver`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/reactive/resource/资源olever.html）实现和[`ResourceTransformer`]（https://DOCS. Spring.io/ Spring/ Spring-framework/DOCS/5.3.16/javoc-api-api-api-api-api/org/spractuframework/org/resource/resource/resource/resolver.html）

你可以使用`VersionResourceResolver`实现基于内容、固定应用程序版本或其他信息计算的 MD5 散列的版本管理的资源 URL。a`ContentVersionStrategy`（md5hash）是一个很好的选择，但有一些明显的例外（例如与模块装入器一起使用的 爪哇Script 资源）。

下面的示例展示了如何在 爪哇 配置中使用`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("/**"));
    }

}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
    }

}
```

你可以使用`ResourceUrlProvider`重写 URL，并应用完整的解析器和转换器（例如，用于插入版本）。WebFlux 配置提供了一个`ResourceUrlProvider`，以便可以将其注入到其他配置中。

Spring 与 MVC 不同的是，目前，在 WebFlux 中，还没有透明地重写静态资源 URL 的方法，因为还没有视图技术可以利用解析器和转换器的非阻塞链。当只提供本地资源时，解决方法是直接使用`ResourceUrlProvider`（例如，通过自定义元素）和块。

请注意，当同时使用`EncodedResourceResolver`（例如，gzip，Brotli 编码）和`VersionedResourceResolver`时，它们必须按该顺序进行注册，以确保始终基于未编码文件可靠地计算基于内容的版本。

[WebJars](https://www.webjars.org/documentation)也通过`WebJarsResourceResolver`来支持，这是在 Classpath 上存在`org.webjars:webjars-locator-core`库时自动注册的。解析器可以重写 URL 以包括 jar 的版本，也可以匹配没有版本的传入 URL——例如，从`/jquery/jquery.min.js`到`/jquery/1.2.0/jquery.min.js`。

|   |基于`ResourceHandlerRegistry`的 爪哇 配置为细粒度控制提供了进一步的选项<br/>，例如，上次修改行为和优化的资源解析。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.11.9.路径匹配

[Web MVC](web.html#mvc-config-path-matching)

你可以自定义与路径匹配相关的选项。有关单个选项的详细信息，请参见[`PathMatchConfigurer`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/active/config/pathmatchconfigrer.html）爪哇doc。下面的示例展示了如何使用`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));
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    @Override
    fun configurePathMatch(configurer: PathMatchConfigurer) {
        configurer
            .setUseCaseSensitiveMatch(true)
            .setUseTrailingSlashMatch(false)
            .addPathPrefix("/api",
                    HandlerTypePredicate.forAnnotation(RestController::class.java))
    }
}
```

|   |Spring WebFlux 依赖于对名为`RequestPath`的请求路径的解析表示，用于访问已解码的路径段值，并删除带有分号内容的<br/>（即路径或矩阵变量）。这意味着，与 Spring MVC 不同，你不需要指示<br/>是否要对请求路径进行解码，也不需要指示是否要出于<br/>路径匹配的目的删除分号内容。<br/><br/> Spring WebFlux 也不支持后缀模式匹配，这与 Spring MVC 不同，其中，我们<br/>也是[recommend](web.html#mvc-ann-requestmapping-suffix-pattern-match)远离<br/>对它的依赖。|
|---| |

#### 1.11.10.WebSocketService

WebFlux 爪哇 Config 声明了一个`WebSocketHandlerAdapter` Bean，它为 WebSocket 处理程序的调用提供了支持。这意味着，要处理 WebSocket 握手请求，仅需通过`SimpleUrlHandlerMapping`将`WebSocketHandler`映射到一个 URL。

在某些情况下，可能需要创建带有所提供的`WebSocketHandlerAdapter` Bean 的`WebSocketService`服务，该服务允许配置 WebSocket 服务器属性。例如：

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public WebSocketService getWebSocketService() {
        TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
        strategy.setMaxSessionIdleTimeout(0L);
        return new HandshakeWebSocketService(strategy);
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    @Override
    fun webSocketService(): WebSocketService {
        val strategy = TomcatRequestUpgradeStrategy().apply {
            setMaxSessionIdleTimeout(0L)
        }
        return HandshakeWebSocketService(strategy)
    }
}
```

#### 1.11.11.高级配置模式

[Web MVC](web.html#mvc-config-advanced-java)

`@EnableWebFlux`Imports`DelegatingWebFluxConfiguration`表示：

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

* 检测并委托`WebFluxConfigurer`实现来定制该配置。

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

爪哇

```
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {

    // ...
}
```

Kotlin

```
@Configuration
class WebConfig : DelegatingWebFluxConfiguration {

    // ...
}
```

你可以在`WebConfig`中保留现有的方法，但是你现在也可以重写 Bean 来自基类的声明，并且在 Classpath 上仍然具有任何数量的其他`WebMvcConfigurer`实现。

### 1.12.http/2

[Web MVC](web.html#mvc-http2)

Tomcat、 Jetty 和 Undertow 支持 HTTP/2。但是，有一些与服务器配置相关的考虑因素。有关更多详细信息，请参见[HTTP/2Wiki 页面](https://github.com/spring-projects/spring-framework/wiki/HTTP-2-support)。

## 2. WebClient

Spring WebFlux 包括用于执行 HTTP 请求的客户端。`WebClient`具有功能强大的、基于 Reactor 的 Fluent API，参见[反应库](#webflux-reactive-libraries)，它使异步逻辑的声明式组合无需处理线程或并发性。它是完全非阻塞的，它支持流媒体，并且依赖同样的[codecs](#webflux-codecs)，这些也用于在服务器端对请求和响应内容进行编码和解码。

`WebClient`需要一个 HTTP 客户库来执行请求。以下是内置的支持：

* [反应堆网状结构](https://github.com/reactor/reactor-netty)

* [Jetty Reactive HttpClient](https://github.com/jetty-project/jetty-reactive-httpclient)

* [Apache HttpComponents](https://hc.apache.org/index.html)

* 其他的可以通过`ClientHttpConnector`进行插接。

### 2.1.配置

创建`WebClient`的最简单方法是通过一种静态工厂方法：

* `WebClient.create()`

* `WebClient.create(String baseUrl)`

你还可以使用`WebClient.builder()`和其他选项：

* `uriBuilderFactory`:定制`UriBuilderFactory`用作基本 URL。

* `defaultUriVariables`:展开 URI 模板时要使用的默认值。

* `defaultHeader`:每个请求的标题。

* `defaultCookie`:每个请求都有 cookies。

* `defaultRequest`:`Consumer`来定制每个请求。

* `filter`:每个请求的客户端过滤器。

* `exchangeStrategies`:HTTP 消息阅读器/Writer 自定义。

* `clientConnector`:http 客户库设置。

例如：

爪哇

```
WebClient client = WebClient.builder()
        .codecs(configurer -> ... )
        .build();
```

Kotlin

```
val webClient = WebClient.builder()
        .codecs { configurer -> ... }
        .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
```

Kotlin

```
val client1 = WebClient.builder()
        .filter(filterA).filter(filterB).build()

val client2 = client1.mutate()
        .filter(filterC).filter(filterD).build()

// client1 has filterA, filterB

// client2 has filterA, filterB, filterC, filterD
```

#### 2.1.1.MaxInMemorySize

编解码器有[limits](#webflux-codecs-limits)用于缓冲内存中的数据，以避免应用程序内存问题。默认情况下，这些被设置为 256KB。如果这还不够，那么你将得到以下错误：

```
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
```

要更改默认编解码器的限制，请使用以下方法：

爪哇

```
WebClient webClient = WebClient.builder()
        .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
        .build();
```

Kotlin

```
val webClient = WebClient.builder()
        .codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) }
        .build()
```

#### 2.1.2.反应堆网状结构

要定制反应堆网络设置，请提供预先配置的`HttpClient`:

爪哇

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

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

Kotlin

```
val httpClient = HttpClient.create().secure { ... }

val webClient = WebClient.builder()
    .clientConnector(ReactorClientHttpConnector(httpClient))
    .build()
```

##### Resources

默认情况下，`HttpClient`参与在`reactor.netty.http.HttpResources`中持有的全局反应器网络资源，包括事件循环线程和连接池。这是推荐的模式，因为对于事件循环并发，首选的是固定的共享资源。在这种模式下，全局资源在流程退出之前一直处于活动状态。

如果服务器与进程同步，则通常不需要显式关机。然而，如果服务器可以在进程中启动或停止（例如， Spring MVC 应用程序部署为 WAR），可以用`globalResources=true`（默认）声明类型`ReactorResourceFactory`的 Spring-管理的 Bean，以确保在 Spring `ApplicationContext`关闭时关闭反应堆网络全局资源，如下例所示：

爪哇

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

Kotlin

```
@Bean
fun reactorResourceFactory() = ReactorResourceFactory()
```

你也可以选择不参与全球反应堆网状资源。但是，在这种模式下，要确保所有 Reactor Netty 客户机和服务器实例都使用共享资源是你的责任，如下例所示：

爪哇

```
@Bean
public ReactorResourceFactory resourceFactory() {
    ReactorResourceFactory factory = new ReactorResourceFactory();
    factory.setUseGlobalResources(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`。|

Kotlin

```
@Bean
fun resourceFactory() = ReactorResourceFactory().apply {
    isUseGlobalResources = false (1)
}

@Bean
fun webClient(): WebClient {

    val mapper: (HttpClient) -> HttpClient = {
        // Further customizations...
    }

    val connector = ReactorClientHttpConnector(resourceFactory(), mapper) (2)

    return WebClient.builder().clientConnector(connector).build() (3)
}
```

|**1**|创造独立于全球资源的资源。|
|-----|-----------------------------------------------------------------------|
|**2**|在资源工厂中使用`ReactorClientHttpConnector`构造函数。|
|**3**|将连接器插入`WebClient.Builder`。|

##### 超时

要配置连接超时：

爪哇

```
import io.netty.channel.ChannelOption;

HttpClient httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);

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

Kotlin

```
import io.netty.channel.ChannelOption

val httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);

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

要配置读或写超时：

爪哇

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

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

// Create WebClient...
```

Kotlin

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

val httpClient = HttpClient.create()
        .doOnConnected { conn -> conn
                .addHandlerLast(new ReadTimeoutHandler(10))
                .addHandlerLast(new WriteTimeoutHandler(10))
        }

// Create WebClient...
```

要为所有请求配置响应超时：

爪哇

```
HttpClient httpClient = HttpClient.create()
        .responseTimeout(Duration.ofSeconds(2));

// Create WebClient...
```

Kotlin

```
val httpClient = HttpClient.create()
        .responseTimeout(Duration.ofSeconds(2));

// Create WebClient...
```

要为特定请求配置响应超时：

爪哇

```
WebClient.create().get()
        .uri("https://example.org/path")
        .httpRequest(httpRequest -> {
            HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
            reactorRequest.responseTimeout(Duration.ofSeconds(2));
        })
        .retrieve()
        .bodyToMono(String.class);
```

Kotlin

```
WebClient.create().get()
        .uri("https://example.org/path")
        .httpRequest { httpRequest: ClientHttpRequest ->
            val reactorRequest = httpRequest.getNativeRequest<HttpClientRequest>()
            reactorRequest.responseTimeout(Duration.ofSeconds(2))
        }
        .retrieve()
        .bodyToMono(String::class.java)
```

#### 2.1.3. Jetty

下面的示例展示了如何自定义 Jetty `HttpClient`设置：

Java

```
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);

WebClient webClient = WebClient.builder()
        .clientConnector(new JettyClientHttpConnector(httpClient))
        .build();
```

Kotlin

```
val httpClient = HttpClient()
httpClient.cookieStore = ...

val webClient = WebClient.builder()
        .clientConnector(new JettyClientHttpConnector(httpClient))
        .build();
```

默认情况下，`HttpClient`创建自己的资源（`Executor`，`ByteBufferPool`，`Scheduler`），这些资源在进程退出或调用`stop()`之前一直处于活动状态。

你可以在 Jetty 客户机（和服务器）的多个实例之间共享资源，并通过声明类型为`JettyResourceFactory`的 Spring 管理的 Bean 来确保在关闭 Spring `ApplicationContext`时关闭资源，如下例所示：

Java

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

@Bean
public WebClient webClient() {

    HttpClient httpClient = new HttpClient();
    // Further customizations...

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

    return WebClient.builder().clientConnector(connector).build(); (2)
}
```

|**1**|在资源工厂中使用`JettyClientHttpConnector`构造函数。|
|-----|---------------------------------------------------------------------|
|**2**|将连接器插入`WebClient.Builder`。|

Kotlin

```
@Bean
fun resourceFactory() = JettyResourceFactory()

@Bean
fun webClient(): WebClient {

    val httpClient = HttpClient()
    // Further customizations...

    val connector = JettyClientHttpConnector(httpClient, resourceFactory()) (1)

    return WebClient.builder().clientConnector(connector).build() (2)
}
```

|**1**|在资源工厂中使用`JettyClientHttpConnector`构造函数。|
|-----|---------------------------------------------------------------------|
|**2**|将连接器插入`WebClient.Builder`。|

#### 2.1.4.HttpComponents

下面的示例展示了如何定制 Apache HttpComponents`HttpClient`设置：

Java

```
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);

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

Kotlin

```
val client = HttpAsyncClients.custom().apply {
    setDefaultRequestConfig(...)
}.build()
val connector = HttpComponentsClientHttpConnector(client)
val webClient = WebClient.builder().clientConnector(connector).build()
```

### 2.2.`retrieve()`

`retrieve()`方法可用于声明如何提取响应。例如：

Java

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

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

Kotlin

```
val client = WebClient.create("https://example.org")

val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .toEntity<Person>().awaitSingle()
```

或者只得到身体：

Java

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

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

Kotlin

```
val client = WebClient.create("https://example.org")

val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .awaitBody<Person>()
```

要获取已解码对象的流：

Java

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

Kotlin

```
val result = client.get()
        .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
        .retrieve()
        .bodyToFlow<Quote>()
```

默认情况下，4xx 或 5xx 响应会导致`WebClientResponseException`，包括用于特定 HTTP 状态代码的子类。要自定义错误响应的处理，请使用`onStatus`处理程序，如下所示：

Java

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

Kotlin

```
val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError) { ... }
        .onStatus(HttpStatus::is5xxServerError) { ... }
        .awaitBody<Person>()
```

### 2.3.交换

Kotlin 中的`exchangeToMono()`和`exchangeToFlux()`方法（或`awaitExchange { }`和`exchangeToFlow { }`）对于需要更多控制的更高级情况是有用的，例如根据响应状态对响应进行不同的解码：

Java

```
Mono<Person> entityMono = client.get()
        .uri("/persons/1")
        .accept(MediaType.APPLICATION_JSON)
        .exchangeToMono(response -> {
            if (response.statusCode().equals(HttpStatus.OK)) {
                return response.bodyToMono(Person.class);
            }
            else {
                // Turn to error
                return response.createException().flatMap(Mono::error);
            }
        });
```

Kotlin

```
val entity = client.get()
  .uri("/persons/1")
  .accept(MediaType.APPLICATION_JSON)
  .awaitExchange {
        if (response.statusCode() == HttpStatus.OK) {
             return response.awaitBody<Person>()
        }
        else {
             throw response.createExceptionAndAwait()
        }
  }
```

当使用上述方法时，在返回的`Mono`或`Flux`完成后，将检查响应体，如果没有使用它，则释放它，以防止内存和连接泄漏。因此，响应不能在更下游的地方被解码。如果需要，由提供的函数声明如何解码响应。

### 2.4.请求主体

请求主体可以从`ReactiveAdapterRegistry`处理的任何异步类型进行编码，例如`Mono`或 Kotlin 协程`Deferred`，如下例所示：

Java

```
Mono<Person> personMono = ... ;

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

Kotlin

```
val personDeferred: Deferred<Person> = ...

client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .body<Person>(personDeferred)
        .retrieve()
        .awaitBody<Unit>()
```

还可以对对象流进行编码，如下例所示：

Java

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

Kotlin

```
val people: Flow<Person> = ...

client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .body(people)
        .retrieve()
        .awaitBody<Unit>()
```

或者，如果你有实际值，你可以使用`bodyValue`快捷方式，如下例所示：

Java

```
Person person = ... ;

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

Kotlin

```
val person: Person = ...

client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(person)
        .retrieve()
        .awaitBody<Unit>()
```

#### 2.4.1.表单数据

要发送表单数据，可以提供`MultiValueMap<String, String>`作为主体。请注意，内容由`FormHttpMessageWriter`自动设置为`application/x-www-form-urlencoded`。下面的示例展示了如何使用`MultiValueMap<String, String>`:

Java

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

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

Kotlin

```
val formData: MultiValueMap<String, String> = ...

client.post()
        .uri("/path", id)
        .bodyValue(formData)
        .retrieve()
        .awaitBody<Unit>()
```

还可以使用`BodyInserters`在线提供表单数据，如下例所示：

Java

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

Kotlin

```
import org.springframework.web.reactive.function.BodyInserters.*

client.post()
        .uri("/path", id)
        .body(fromFormData("k1", "v1").with("k2", "v2"))
        .retrieve()
        .awaitBody<Unit>()
```

#### 2.4.2.多部分数据

要发送多部分数据，你需要提供一个`MultiValueMap<String, ?>`，其值要么是表示部分内容的`Object`实例，要么是表示部分内容和标题的`HttpEntity`实例。`MultipartBodyBuilder`提供了一个方便的 API 来准备多部分请求。下面的示例展示了如何创建`MultiValueMap<String, ?>`:

Java

```
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // Part from a server request

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

Kotlin

```
val builder = MultipartBodyBuilder().apply {
    part("fieldPart", "fieldValue")
    part("filePart1", new FileSystemResource("...logo.png"))
    part("jsonPart", new Person("Jason"))
    part("myPart", part) // Part from a server request
}

val parts = builder.build()
```

在大多数情况下，你不必为每个部分指定`Content-Type`。内容类型是根据用于序列化它的`HttpMessageWriter`自动确定的，或者在`Resource`的情况下，根据文件扩展名自动确定的。如果有必要，你可以通过重载的构建器`part`方法之一，显式地为每个部分提供`MediaType`。

一旦准备好`MultiValueMap`，将其传递给`WebClient`的最简单方法是通过`body`方法，如下例所示：

Java

```
MultipartBodyBuilder builder = ...;

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

Kotlin

```
val builder: MultipartBodyBuilder = ...

client.post()
        .uri("/path", id)
        .body(builder.build())
        .retrieve()
        .awaitBody<Unit>()
```

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

作为`MultipartBodyBuilder`的替代方案，你还可以通过内置的`BodyInserters`提供内联样式的多部分内容，如下例所示：

Java

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

Kotlin

```
import org.springframework.web.reactive.function.BodyInserters.*

client.post()
        .uri("/path", id)
        .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
        .retrieve()
        .awaitBody<Unit>()
```

### 2.5.过滤器

你可以通过`WebClient.Builder`注册一个客户端过滤器（`ExchangeFilterFunction`），以便拦截和修改请求，如下例所示：

Java

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

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

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

Kotlin

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

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

            next.exchange(filtered)
        }
        .build()
```

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

Java

```
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;

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

Kotlin

```
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication

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

可以通过更改现有的`WebClient`实例来添加或删除过滤器，从而生成一个不影响原始实例的新`WebClient`实例。例如：

Java

```
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;

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

Kotlin

```
val client = webClient.mutate()
        .filters { it.add(0, basicAuthentication("user", "password")) }
        .build()
```

`WebClient`是围绕过滤器链的一个薄的外观，然后是`ExchangeFunction`。它提供了一个工作流，用于发出请求，对来自更高级别的对象进行编码，并有助于确保始终使用响应内容。当过滤器以某种方式处理响应时，必须格外小心，以始终使用其内容，或以其他方式将其向下游传播到`WebClient`，这将确保相同的结果。下面是一个过滤器，它处理`UNAUTHORIZED`状态代码，但确保释放任何响应内容（无论是否期望）：

Java

```
public ExchangeFilterFunction renewTokenFilter() {
    return (request, next) -> next.exchange(request).flatMap(response -> {
        if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
            return response.releaseBody()
                    .then(renewToken())
                    .flatMap(token -> {
                        ClientRequest newRequest = ClientRequest.from(request).build();
                        return next.exchange(newRequest);
                    });
        } else {
            return Mono.just(response);
        }
    });
}
```

Kotlin

```
fun renewTokenFilter(): ExchangeFilterFunction? {
    return ExchangeFilterFunction { request: ClientRequest?, next: ExchangeFunction ->
        next.exchange(request!!).flatMap { response: ClientResponse ->
            if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
                [email protected] response.releaseBody()
                        .then(renewToken())
                        .flatMap { token: String? ->
                            val newRequest = ClientRequest.from(request).build()
                            next.exchange(newRequest)
                        }
            } else {
                [email protected] Mono.just(response)
            }
        }
    }
}
```

### 2.6.属性

你可以向请求添加属性。如果你希望通过筛选链传递信息并影响给定请求的筛选器的行为，这是很方便的。例如：

Java

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

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

    }
```

Kotlin

```
val client = WebClient.builder()
        .filter { request, _ ->
            val usr = request.attributes()["myAttribute"];
            // ...
        }
        .build()

    client.get().uri("https://example.org/")
            .attribute("myAttribute", "...")
            .retrieve()
            .awaitBody<Unit>()
```

请注意，你可以在`WebClient.Builder`级别全局配置`defaultRequest`回调，它允许你将属性插入到所有请求中，例如，可以在 Spring MVC 应用程序中使用它来基于`ThreadLocal`数据填充请求属性。

### 2.7.上下文

[Attributes](#webflux-client-attributes)提供了一种将信息传递到过滤器链的方便方式，但它们只会影响当前的请求。如果你想要传递传播到嵌套的其他请求的信息，例如通过`flatMap`，或者在之后执行，例如通过`concatMap`，那么你将需要使用反应器`Context`。

反应器`Context`需要在反应链的末端填充，以便应用于所有操作。例如：

Java

```
WebClient client = WebClient.builder()
        .filter((request, next) ->
                Mono.deferContextual(contextView -> {
                    String value = contextView.get("foo");
                    // ...
                }))
        .build();

client.get().uri("https://example.org/")
        .retrieve()
        .bodyToMono(String.class)
        .flatMap(body -> {
                // perform nested request (context propagates automatically)...
        })
        .contextWrite(context -> context.put("foo", ...));
```

### 2.8.同步使用

`WebClient`可以以同步方式使用，方法是在末尾对结果进行阻塞：

Java

```
Person person = client.get().uri("/person/{id}", i).retrieve()
    .bodyToMono(Person.class)
    .block();

List<Person> persons = client.get().uri("/persons").retrieve()
    .bodyToFlux(Person.class)
    .collectList()
    .block();
```

Kotlin

```
val person = runBlocking {
    client.get().uri("/person/{id}", i).retrieve()
            .awaitBody<Person>()
}

val persons = runBlocking {
    client.get().uri("/persons").retrieve()
            .bodyToFlow<Person>()
            .toList()
}
```

但是，如果需要进行多个调用，那么更有效的方法是避免单个地阻塞每个响应，而是等待合并的结果：

Java

```
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
        .retrieve().bodyToMono(Person.class);

Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
        .retrieve().bodyToFlux(Hobby.class).collectList();

Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
            Map<String, String> map = new LinkedHashMap<>();
            map.put("person", person);
            map.put("hobbies", hobbies);
            return map;
        })
        .block();
```

Kotlin

```
val data = runBlocking {
        val personDeferred = async {
            client.get().uri("/person/{id}", personId)
                    .retrieve().awaitBody<Person>()
        }

        val hobbiesDeferred = async {
            client.get().uri("/person/{id}/hobbies", personId)
                    .retrieve().bodyToFlow<Hobby>().toList()
        }

        mapOf("person" to personDeferred.await(), "hobbies" to hobbiesDeferred.await())
    }
```

以上只是一个例子。还有很多其他模式和操作员可以将反应性管道组合在一起，从而进行许多远程调用，可能是一些嵌套的、相互依赖的调用，并且直到最后都不会阻塞。

|   |使用`Flux`或`Mono`，你应该永远不需要在 Spring MVC 或 Spring WebFlux 控制器中进行阻塞。<br/>只需从控制器方法返回得到的反应性类型。同样的原理也适用于<br/> Kotlin 协程和 Spring WebFlux，只需使用悬挂函数或在你的`Flow`控制器中返回<br/>方法。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 2.9.测试

要测试使用`WebClient`的代码，可以使用模拟 Web 服务器，例如[OKHTTP MockWebServer](https://github.com/square/okhttp#mockwebserver)。要查看它的使用示例，请查看 Spring Framework 测试套件中的[`WebClientIntegrationTests`]（https://github.com/ Spring-projects/ Spring-framework/tree/main/ Spring-webflux/SRC/test/java/org/springframework/web/active/function/client/webclientintegrationtests.java）或[<gtr="2055"/>]（https:/giaster/tsquare/kmaster/okthub/samples/static-server）存储库中的

## 3. WebSockets

[Same as in the Servlet stack](web.html#websocket)

参考文档的这一部分涵盖了对 Reactive-Stack WebSocket 消息传递的支持。

### 3.1. WebSocket 介绍

WebSocket 协议[RFC 6455](https://tools.ietf.org/html/rfc6455)提供了一种标准化的方式，通过单个 TCP 连接在客户机和服务器之间建立全双工、双向通信通道。它是一种与 HTTP 不同的 TCP 协议，但其设计是通过 HTTP 工作的，使用端口 80 和 443，并允许重用现有的防火墙规则。

WebSocket 交互以 HTTP 请求开始，该 HTTP 请求使用 HTTP头来升级或在这种情况下切换到 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**|`Upgrade`标头。|
|-----|-------------------------------|
|**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 套接字仍然是开放的，以便客户机和服务器继续发送和接收消息。

关于 WebSockets 如何工作的完整介绍超出了本文的范围。参见 RFC6455，HTML5 的 WebSocket 章，或者 Web 上的许多介绍和教程中的任何一个。

注意，如果 WebSocket 服务器运行在 Web 服务器（例如 Nginx）的后面，则可能需要将其配置为将 WebSocket 升级请求传递到 WebSocket 服务器。同样，如果应用程序在云环境中运行，则检查与 WebSocket 支持相关的云提供商的指令。

#### 3.1.1.HTTP 与 WebSocket

尽管 WebSocket 的设计是与 HTTP 兼容的，并且以 HTTP 请求开始，但重要的是要理解这两个协议导致了非常不同的体系结构和应用程序编程模型。

在 HTTP 和 REST 中，应用程序被建模为许多 URL。为了与应用程序交互，客户端访问这些 URL，请求-响应样式。服务器根据 HTTP URL、方法和标头将请求路由到适当的处理程序。

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

WebSocket 也是一种低级传输协议，它与 HTTP 不同，不对消息的内容规定任何语义。这意味着，除非客户机和服务器在消息语义上达成一致，否则就没有路由或处理消息的方法。

WebSocket 客户端和服务器可以协商使用更高级别的消息传递协议（例如，STOMP），通过Header 上的 HTTP 握手请求。如果不能做到这一点，他们就需要拿出自己的惯例。

#### 3.1.2.何时使用 WebSockets

WebSockets 可以使 Web 页面具有动态性和交互性。然而，在许多情况下，Ajax 和 HTTP 流或长轮询的组合可以提供简单有效的解决方案。

例如，新闻、邮件和社交提要需要动态更新，但每隔几分钟更新一次可能完全没问题。另一方面，协作、游戏和金融应用程序需要更接近实时。

延迟本身并不是一个决定因素。如果消息量相对较低（例如，监视网络故障），则 HTTP 流或轮询可以提供有效的解决方案。正是低延迟、高频率和高音量的组合为 WebSocket 的使用提供了最佳的条件。

还请记住，在 Internet 上，超出你控制范围的限制性代理可能会阻止 WebSocket 交互，这是因为它们未被配置为传递`Upgrade`头，或者是因为它们关闭了似乎空闲的长期连接。这意味着对防火墙内的内部应用程序使用 WebSocket 比对面向公众的应用程序使用 WebSocket 是一个更直接的决策。

### 3.2. WebSocket API

[Same as in the Servlet stack](web.html#websocket-server)

Spring 框架提供了一个 WebSocket API，你可以使用它编写处理 WebSocket 消息的客户端和服务器端应用程序。

#### 3.2.1.服务器

[Same as in the Servlet stack](web.html#websocket-server-handler)

要创建 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) {
        // ...
    }
}
```

Kotlin

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

class MyWebSocketHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {
        // ...
    }
}
```

然后你可以将它映射到一个 URL:

爪哇

```
@Configuration
class WebConfig {

    @Bean
    public HandlerMapping handlerMapping() {
        Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/path", new MyWebSocketHandler());
        int order = -1; // before annotated controllers

        return new SimpleUrlHandlerMapping(map, order);
    }
}
```

Kotlin

```
@Configuration
class WebConfig {

    @Bean
    fun handlerMapping(): HandlerMapping {
        val map = mapOf("/path" to MyWebSocketHandler())
        val order = -1 // before annotated controllers

        return SimpleUrlHandlerMapping(map, order)
    }
}
```

如果使用[WebFlux 配置](#webflux-config)，则没有更多的事情要做，或者如果不使用 WebFlux 配置，则需要声明`WebSocketHandlerAdapter`，如下所示：

爪哇

```
@Configuration
class WebConfig {

    // ...

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

Kotlin

```
@Configuration
class WebConfig {

    // ...

    @Bean
    fun handlerAdapter() =  WebSocketHandlerAdapter()
}
```

#### 3.2.2.`WebSocketHandler`

`handle`的`WebSocketHandler`方法接受`WebSocketSession`并返回`Mono<Void>`，以指示会话的应用程序处理完成时。会话通过两个流处理，一个用于入站消息，另一个用于出站消息。下表描述了处理流的两种方法：

|          `WebSocketSession` method           |说明|
|----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|      `Flux<WebSocketMessage> receive()`      |提供对入站消息流的访问，并在连接关闭时完成。|
|`Mono<Void> send(Publisher<WebSocketMessage>)`|获取传出消息的源，写入消息，并返回一个`Mono<Void>`，当源完成并写入完成时，该<br/>完成。|

a`WebSocketHandler`必须将入站和出站流组合成一个统一的流，并返回一个`Mono<Void>`，该流反映了该流的完成。根据应用程序的需求，统一流在以下情况下完成：

* 入站消息流或出站消息流已完成。

* 入站流完成（即连接关闭），而出站流是无限的。

* 在选定的点上，通过`close`的`WebSocketSession`方法。

当入站和出站消息流组合在一起时，不需要检查连接是否打开，因为反应流信号结束活动。入站流接收完成或错误信号，出站流接收取消信号。

处理程序的最基本实现是处理入站流的实现。下面的示例展示了这样的实现：

爪哇

```
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>`。|

Kotlin

```
class ExampleHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {
        return session.receive()            (1)
                .doOnNext {
                    // ...                  (2)
                }
                .concatMap {
                    // ...                  (3)
                }
                .then()                     (4)
    }
}
```

|**1**|访问入站消息流。|
|-----|--------------------------------------------------------------------|
|**2**|对每条信息都做些什么。|
|**3**|执行使用消息内容的嵌套异步操作。|
|**4**|返回在接收完成时完成的`Mono<Void>`。|

|   |对于嵌套的异步操作，你可能需要在使用池数据缓冲区的底层<br/>服务器（例如 Netty）上调用`message.retain()`。否则，数据缓冲区可能会在有机会读取数据之前<br/>释放。有关更多背景信息，请参见[数据缓冲区和编解码器](core.html#databuffers)。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

以下实现合并了入站和出站流：

爪哇

```
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>`，它在我们继续接收时不完成。|

Kotlin

```
class ExampleHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {

        val output = session.receive()                      (1)
                .doOnNext {
                    // ...
                }
                .concatMap {
                    // ...
                }
                .map { session.textMessage("Echo $it") }    (2)

        return session.send(output)                         (3)
    }
}
```

|**1**|处理入站消息流。|
|-----|--------------------------------------------------------------------------|
|**2**|创建出站消息，生成一个合并的流。|
|**3**|返回一个`Mono<Void>`，它在我们继续接收时不完成。|

入站和出站流可以是独立的，并且仅在完成时才加入，如下例所示：

爪哇

```
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>`，当任一流结束时完成。|

Kotlin

```
class ExampleHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {

        val input = session.receive()                                   (1)
                .doOnNext {
                    // ...
                }
                .concatMap {
                    // ...
                }
                .then()

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

        return Mono.zip(input, output).then()                           (3)
    }
}
```

|**1**|处理入站消息流。|
|-----|----------------------------------------------------------------------------------|
|**2**|发送外发消息。|
|**3**|加入这些流并返回一个`Mono<Void>`，当任一流结束时完成。|

#### 3.2.3.`DataBuffer`

`DataBuffer`是 WebFlux 中字节缓冲区的表示形式。参考的 Spring 核心部分在[数据缓冲区和编解码器](core.html#databuffers)一节中对此有更多的说明。要理解的关键点是，在某些服务器（如 Netty）上，字节缓冲区是池的，引用也是计算的，并且必须在使用时释放，以避免内存泄漏。

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

#### 3.2.4.握手

[Same as in the Servlet stack](web.html#websocket-server-handshake)

`WebSocketHandlerAdapter`委托给`WebSocketService`。默认情况下，这是`HandshakeWebSocketService`的一个实例，它对 WebSocket 请求执行基本检查，然后对正在使用的服务器使用`RequestUpgradeStrategy`。目前，有对反应堆网状物、 Tomcat、 Jetty 和 Undertow 的内置支持。

`HandshakeWebSocketService`公开了一个`sessionAttributePredicate`属性，该属性允许设置`Predicate<String>`以从`WebSession`中提取属性，并将它们插入到`WebSocketSession`的属性中。

#### 3.2.5.服务器配置

[Same as in the Servlet stack](web.html#websocket-server-runtime-configuration)

每个服务器的`RequestUpgradeStrategy`公开了特定于底层 WebSocket 服务器引擎的配置。当使用 WebFlux 爪哇 Config 时，你可以自定义这些属性，如[WebFlux 配置](#webflux-config-websocket-service)的相应部分所示，或者如果不使用 WebFlux Config，则使用以下方法：

爪哇

```
@Configuration
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);
    }
}
```

Kotlin

```
@Configuration
class WebConfig {

    @Bean
    fun handlerAdapter() =
            WebSocketHandlerAdapter(webSocketService())

    @Bean
    fun webSocketService(): WebSocketService {
        val strategy = TomcatRequestUpgradeStrategy().apply {
            setMaxSessionIdleTimeout(0L)
        }
        return HandshakeWebSocketService(strategy)
    }
}
```

检查你的服务器的升级策略，看看有哪些选项可用。目前，只有 Tomcat 和 Jetty 公开了这样的选项。

#### 3.2.6.科尔斯

[Same as in the Servlet stack](web.html#websocket-server-allowed-origins)

配置 CORS 并限制对 WebSocket 端点的访问的最简单方法是让你的`WebSocketHandler`实现`CorsConfigurationSource`并返回一个`CorsConfiguration`，其中包含允许的源代码、标头和其他详细信息。如果无法做到这一点，还可以在`SimpleUrlHandler`上设置`corsConfigurations`属性，以通过 URL 模式指定 CORS 设置。如果指定了这两个参数，则使用`combine`上的`CorsConfiguration`方法将它们合并。

#### 3.2.7.客户

Spring WebFlux 提供了一个`WebSocketClient`抽象，其中包含用于反应器 Netty、 Tomcat、 Jetty、 Undertow 和标准 爪哇（即 JSR-356）的实现。

|   |Tomcat 客户机实际上是标准 爪哇 One 的扩展，它在`WebSocketSession`处理中具有一些额外的<br/>功能，以利用 Tomcat 特定的<br/>API 来暂停接收用于回压的消息。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

要启动 WebSocket 会话，你可以创建客户机的实例，并使用其`execute`方法：

爪哇

```
WebSocketClient client = new ReactorNettyWebSocketClient();

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

Kotlin

```
val client = ReactorNettyWebSocketClient()

        val url = URI("ws://localhost:8080/path")
        client.execute(url) { session ->
            session.receive()
                    .doOnNext(::println)
            .then()
        }
```

一些客户机，例如 Jetty，实现了`Lifecycle`，并且需要在可以使用它们之前停止并启动它们。所有客户端都具有与底层 WebSocket 客户端的配置相关的构造函数选项。

## 4. 测试

[Same in Spring MVC](web.html#testing)

`spring-test`模块提供了`ServerHttpRequest`、`ServerHttpResponse`和`ServerWebExchange`的模拟实现。有关模拟对象的讨论，请参见[Spring Web Reactive](testing.html#mock-objects-web-reactive)。

[`WebTestClient`]（Testing.html#WebTestClient）构建在这些模拟请求和响应对象上，以提供对无需 HTTP 服务器的 WebFlux 应用程序的测试支持。对于端到端集成测试，也可以使用`WebTestClient`。

## 5. RSocket

本节描述 Spring Framework 对 RSocket 协议的支持。

### 5.1.概述

WebSocket RSocket 是一种用于在 TCP、 WebSocket 和其他字节流传输上进行多路复用、双工通信的应用程序协议，它使用以下交互模型之一：

* `Request-Response`—发送一条消息，接收一条返回。

* `Request-Stream`—发送一条消息并接收一系列消息。

* `Channel`——双向发送消息流。

* `Fire-and-Forget`—发送单向消息。

一旦建立了初始连接，“客户端”与“服务器”的区别就会丢失，因为双方变得对称，并且双方都可以启动上述交互之一。这就是为什么在协议中将参与方称为“请求者”和“响应者”，而上述交互称为“请求流”或简称“请求”。

这些是 RSocket 协议的关键特性和优点：

* 跨越网络边界的[反应流](https://www.reactive-streams.org/)语义——对于`Request-Stream`和`Channel`之类的流媒体请求，背压信号在请求者和响应者之间传输，从而允许请求者在源处减慢响应者的速度，从而减少对网络层拥塞控制的依赖，以及在网络级别或任何级别上对缓冲的需求。

* 请求节流——这个特性在`LEASE`帧之后被命名为“租赁”，该帧可以从每一端发送，以限制另一端在给定时间内允许的请求总数。租约定期续签。

* 会话恢复——这是为失去连接而设计的，并且需要保持某些状态。状态管理对于应用程序是透明的，并且与背压相结合工作得很好，背压可以在可能的情况下停止生产者并减少所需的状态量。

* 大消息的分片和重新组装。

* KeepAlive（心跳）。

RSocket 在多种语言中都有[实现](https://github.com/rsocket)。[爪哇 库](https://github.com/rsocket/rsocket-java)是建立在[项目反应堆](https://projectreactor.io/)上的，而[反应堆网状结构](https://github.com/reactor/reactor-netty)是用于传输的。这意味着来自应用程序中的反应流发布者的信号通过 RSocket 在整个网络中透明地传播。

#### 5.1.1.《议定书》

RSocket 的优点之一是它在有线上具有定义良好的行为，以及易于读取的[规格](https://rsocket.io/docs/Protocol)以及一些协议[extensions](https://github.com/rsocket/rsocket/tree/master/Extensions)。因此，阅读规范是一个好主意，独立于语言实现和更高级别的框架 API。本节提供了一个简明的概述，以建立一些上下文。

**连接**

最初，客户端通过一些低级别的流媒体传输（例如 TCP 或 WebSocket）连接到服务器，并向服务器发送`SETUP`帧，以设置连接的参数。

服务器可能会拒绝`SETUP`帧，但通常在它被发送（对于客户端）和接收（对于服务器）之后，双方都可以开始进行请求，除非`SETUP`表示使用租赁语义来限制请求的数量，在这种情况下，双方都必须等待来自另一端的`LEASE`帧以允许进行请求。

**提出要求**

一旦建立了连接，双方可以通过`REQUEST_RESPONSE`、`REQUEST_STREAM`、`REQUEST_CHANNEL`或`REQUEST_FNF`中的一个帧发起请求。这些帧中的每一个都从请求者向响应者传送一条消息。

然后响应者可以返回带有响应消息的`PAYLOAD`帧，并且在`REQUEST_CHANNEL`的情况下，请求者还可以发送带有更多请求消息的`PAYLOAD`帧。

当请求涉及诸如`Request-Stream`和`Channel`之类的消息流时，响应者必须尊重来自请求者的需求信号。需求被表示为大量的消息。初始需求在`REQUEST_STREAM`和`REQUEST_CHANNEL`框架中指定。后续的需求是通过`REQUEST_N`帧来表示的。

每一方也可以通过`METADATA_PUSH`帧发送元数据通知，这些通知与任何单独的请求无关，而是与整个连接有关。

**消息格式**

RSocket 消息包含数据和元数据。元数据可用于发送路由、安全令牌等。数据和元数据可以采用不同的格式。每个类型的 MIME 类型都在`SETUP`框架中声明，并应用于给定连接上的所有请求。

虽然所有消息都可以具有元数据，但通常的元数据例如路由是每个请求的，因此仅包含在请求的第一个消息中，即具有一个框架`REQUEST_RESPONSE`，`REQUEST_STREAM`，`REQUEST_CHANNEL`，或`REQUEST_FNF`。

协议扩展定义了应用程序中使用的通用元数据格式：

* [复合元数据](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md)--多个独立格式化的元数据条目。

* [Routing](https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md)—请求的路径。

#### 5.1.2.爪哇 实现

RSocket 的[爪哇 实现](https://github.com/rsocket/rsocket-java)是建立在[项目反应堆](https://projectreactor.io/)之上的。TCP 和 WebSocket 的传输建立在[反应堆网状结构](https://github.com/reactor/reactor-netty)上。作为一种反应流库，Reactor 简化了协议的实现工作。对于应用程序来说，使用`Flux`和`Mono`声明运算符和透明背压支持是很自然的。

RSocket 爪哇 中的 API 有意地是最小的和基本的。它专注于协议特性，并将应用程序编程模型（例如 RPC Codegen vs Other）作为更高级别的独立关注点。

主契约[io.rsocket.rsocket](https://github.com/rsocket/rsocket-java/blob/master/rsocket-core/src/main/java/io/rsocket/RSocket.java)用`Mono`表示对单个消息的承诺、`Flux`消息流和`io.rsocket.Payload`实际消息进行建模，并将对数据和元数据的访问作为字节缓冲区。`RSocket`契约是对称使用的。对于请求，给应用程序一个`RSocket`来执行请求。对于响应，应用程序实现`RSocket`来处理请求。

这并不意味着要做一个全面的介绍。 Spring 在大多数情况下，应用程序将不必直接使用其 API。然而，独立于 Spring 来观察或实验 RSocket 可能是重要的。RSocket 爪哇 存储库包含许多[示例应用程序](https://github.com/rsocket/rsocket-java/tree/master/rsocket-examples)，它们演示了它的 API 和协议特性。

#### 5.1.3. Spring 支持

`spring-messaging`模块包含以下内容：

* [Rsocketrequester](#rsocket-requester)—Fluent API 通过带有数据和元数据编码/解码的`io.rsocket.RSocket`进行请求。

* [附加注释的响应者](#rsocket-annot-responders)—`@MessageMapping`用于响应的注释处理程序方法。

`spring-web`模块包含`Encoder`和`Decoder`实现，例如 Jackson 的 cbor/json，以及 RSocket 应用程序可能需要的 Protobuf。它还包含`PathPatternParser`，可以插入该参数以进行有效的路由匹配。

Spring Boot2.2 支持在 TCP 或 WebSocket 上站立 RSocket 服务器，包括在 WebFlux 服务器中公开 RSocket over WebSocket 的选项。对于`RSocketRequester.Builder`和`RSocket战略`也有客户机支持和自动配置。有关更多详细信息，请参见 Spring 引导引用中的[RSocket 部分](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-rsocket)。

Spring 安全性 5.2 提供了 RSocket 支持。

Spring 集成 5.2 提供了入站和出站网关，以与 RSocket 客户端和服务器进行交互。有关更多详细信息，请参见 Spring 集成参考手册。

Spring 云网关支持 RSocket 连接。

### 5.2.Rsocketrequester

`RSocketRequester`提供了一个 Fluent API 来执行 RSocket 请求，接受并返回数据和元数据的对象，而不是低级别的数据缓冲区。它可以被对称地使用，用于从客户机发出请求和从服务器发出请求。

#### 5.2.1.客户请求者

要在客户端获得`RSocketRequester`，就需要连接到一个服务器，该服务器需要发送一个带有连接设置的 RSocket`SETUP`帧。`RSocketRequester`提供了一个构建器，该构建器帮助准备一个`io.rsocket.core.RSocketConnector`，包括`SETUP`框架的连接设置。

这是连接默认设置的最基本方式：

爪哇

```
RSocketRequester requester = RSocketRequester.builder().tcp("localhost", 7000);

URI url = URI.create("https://example.org:8080/rsocket");
RSocketRequester requester = RSocketRequester.builder().webSocket(url);
```

Kotlin

```
val requester = RSocketRequester.builder().tcp("localhost", 7000)

URI url = URI.create("https://example.org:8080/rsocket");
val requester = RSocketRequester.builder().webSocket(url)
```

上面的连接不是立即的。当提出请求时，将透明地建立并使用共享连接。

##### 连接设置

`RSocketRequester.Builder`提供了以下自定义初始化`SETUP`框架的方法：

* `dataMimeType(MimeType)`—为连接上的数据设置 MIME 类型。

* `metadataMimeType(MimeType)`—为连接上的元数据设置 MIME 类型。

* `setupData(Object)`—要包含在`SETUP`中的数据。

* `setupRoute(String, Object…​)`—将元数据中的路由包含在`SETUP`中。

* `setupMetadata(Object, MimeType)`—要包含在`SETUP`中的其他元数据。

对于数据，默认的 MIME 类型是从第一个配置的`Decoder`派生的。对于元数据，默认的 MIME 类型是[复合元数据](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md)，它允许每个请求有多个元数据值和 MIME 类型对。通常情况下，这两种情况都不需要改变。

`SETUP`框架中的数据和元数据是可选的。在服务器端，[@ConnectMapping](#rsocket-annot-connectmapping)方法可用于处理连接的开始和`SETUP`框架的内容。元数据可用于连接级别的安全性。

##### Strategies

`RSocketRequester.Builder`接受`RSocketStrategies`来配置请求者。你将需要使用它来为数据和元数据值的（反）序列化提供编码器和解码器。默认情况下，只注册来自`spring-core`的`String`、`byte[]`和`ByteBuffer`的基本编解码器。添加`spring-web`可以访问更多可以按以下方式注册的内容：

爪哇

```
RSocketStrategies strategies = RSocketStrategies.builder()
    .encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
    .decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
    .build();

RSocketRequester requester = RSocketRequester.builder()
    .rsocketStrategies(strategies)
    .tcp("localhost", 7000);
```

Kotlin

```
val strategies = RSocketStrategies.builder()
        .encoders { it.add(Jackson2CborEncoder()) }
        .decoders { it.add(Jackson2CborDecoder()) }
        .build()

val requester = RSocketRequester.builder()
        .rsocketStrategies(strategies)
        .tcp("localhost", 7000)
```

`RSocketStrategies`是为重复使用而设计的。在一些场景中，例如客户端和服务器在相同的应用程序中，可以优选地在 Spring 配置中对其进行声明。

##### 客户响应者

`RSocketRequester.Builder`可用于配置来自服务器的请求的响应程序。

你可以使用带注释的处理程序进行客户端响应，该处理程序基于服务器上使用的相同基础设施，但以编程方式注册，如下所示：

爪哇

```
RSocketStrategies strategies = RSocketStrategies.builder()
    .routeMatcher(new PathPatternRouteMatcher())  (1)
    .build();

SocketAcceptor responder =
    RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)

RSocketRequester requester = RSocketRequester.builder()
    .rsocketConnector(connector -> connector.acceptor(responder)) (3)
    .tcp("localhost", 7000);
```

|**1**|如果存在`spring-web`，则使用`PathPatternRouteMatcher`，以进行有效的<br/>路由匹配。|
|-----|--------------------------------------------------------------------------------------------|
|**2**|从具有`@MessageMaping`和/或`@ConnectMapping`方法的类创建响应器。|
|**3**|登记响应者。|

Kotlin

```
val strategies = RSocketStrategies.builder()
        .routeMatcher(PathPatternRouteMatcher())  (1)
        .build()

val responder =
    RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)

val requester = RSocketRequester.builder()
        .rsocketConnector { it.acceptor(responder) } (3)
        .tcp("localhost", 7000)
```

|**1**|如果存在`spring-web`，则使用`PathPatternRouteMatcher`，以进行有效的<br/>路由匹配。|
|-----|--------------------------------------------------------------------------------------------|
|**2**|从具有`@MessageMaping`和/或`@ConnectMapping`方法的类创建响应器。|
|**3**|登记响应者。|

注意，上面只是为客户端响应者的程序化注册设计的一个快捷方式。对于客户机响应者处于 Spring 配置中的替代场景，你仍然可以将`RSocketMessageHandler`声明为 Spring  Bean，然后按以下方式应用：

爪哇

```
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);

RSocketRequester requester = RSocketRequester.builder()
    .rsocketConnector(connector -> connector.acceptor(handler.responder()))
    .tcp("localhost", 7000);
```

Kotlin

```
import org.springframework.beans.factory.getBean

val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()

val requester = RSocketRequester.builder()
        .rsocketConnector { it.acceptor(handler.responder()) }
        .tcp("localhost", 7000)
```

对于上述情况，还可能需要使用`setHandlerPredicate`中的`RSocketMessageHandler`来切换到用于检测客户端响应者的不同策略，例如基于诸如`@RSocketClientResponder`VS 的默认`@Controller`的自定义注释。在使用客户机和服务器，或者在同一个应用程序中有多个客户机的情况下，这是必要的。

有关编程模型的更多信息，请参见[附加注释的响应者](#rsocket-annot-responders)。

##### 高级

`RSocketRequesterBuilder`提供了一个回调，以公开底层`io.rsocket.core.RSocketConnector`的更多配置选项，用于保持活动间隔、会话恢复、拦截器等。你可以按以下方式配置该级别的选项：

爪哇

```
RSocketRequester requester = RSocketRequester.builder()
    .rsocketConnector(connector -> {
        // ...
    })
    .tcp("localhost", 7000);
```

Kotlin

```
val requester = RSocketRequester.builder()
        .rsocketConnector {
            //...
        }
        .tcp("localhost", 7000)
```

#### 5.2.2.服务器请求者

要从服务器向连接的客户机发出请求，需要从服务器获得连接的客户机的请求者。

在[附加注释的响应者](#rsocket-annot-responders)中，`@ConnectMapping`和`@MessageMapping`方法支持`RSocketRequester`参数。使用它来访问连接的请求者。请记住，`@ConnectMapping`方法本质上是`SETUP`框架的处理程序，在开始请求之前必须对其进行处理。因此，从一开始就必须将请求与处理分离开来。例如：

爪哇

```
@ConnectMapping
Mono<Void> handle(RSocketRequester requester) {
    requester.route("status").data("5")
        .retrieveFlux(StatusReport.class)
        .subscribe(bar -> { (1)
            // ...
        });
    return ... (2)
}
```

|**1**|异步启动请求，与处理无关。|
|-----|------------------------------------------------------------|
|**2**|执行处理并返回完成`Mono<Void>`。|

Kotlin

```
@ConnectMapping
suspend fun handle(requester: RSocketRequester) {
    GlobalScope.launch {
        requester.route("status").data("5").retrieveFlow<StatusReport>().collect { (1)
            // ...
        }
    }
    /// ... (2)
}
```

|**1**|异步启动请求，与处理无关。|
|-----|------------------------------------------------------------|
|**2**|在挂起功能中执行处理。|

#### 5.2.3.请求

一旦有了[client](#rsocket-requester-client)或[server](#rsocket-requester-server)请求者，你可以按以下方式进行请求：

爪哇

```
ViewBox viewBox = ... ;

Flux<AirportLocation> locations = requester.route("locate.radars.within") (1)
        .data(viewBox) (2)
        .retrieveFlux(AirportLocation.class); (3)
```

|**1**|指定要包含在请求消息的元数据中的路由。|
|-----|------------------------------------------------------------------|
|**2**|为请求消息提供数据。|
|**3**|声明预期的响应。|

Kotlin

```
val viewBox: ViewBox = ...

val locations = requester.route("locate.radars.within") (1)
        .data(viewBox) (2)
        .retrieveFlow<AirportLocation>() (3)
```

|**1**|指定要包含在请求消息的元数据中的路由。|
|-----|------------------------------------------------------------------|
|**2**|为请求消息提供数据。|
|**3**|声明预期的响应。|

交互类型是由输入和输出的基数隐式确定的。上面的示例是`Request-Stream`，因为发送了一个值并接收了一个值流。在大多数情况下，只要输入和输出的选择与 RSocket 交互类型以及响应者期望的输入和输出类型相匹配，就不需要考虑这个问题。无效组合的唯一示例是多对一。

`data(Object)`方法还接受任何活性流`Publisher`，包括`Flux`和`Mono`，以及在`ReactiveAdapterRegistry`中注册的任何其他值的生成器。对于产生相同类型的值的多值`Publisher`，例如`Flux`，可以考虑使用重载的`data`方法之一，以避免对每个元素进行类型检查和`Encoder`查找：

```
data(Object producer, Class<?> elementClass);
data(Object producer, ParameterizedTypeReference<?> elementTypeRef);
```

`data(Object)`步骤是可选的。对于不发送数据的请求，跳过它：

爪哇

```
Mono<AirportLocation> location = requester.route("find.radar.EWR"))
    .retrieveMono(AirportLocation.class);
```

Kotlin

```
import org.springframework.messaging.rsocket.retrieveAndAwait

val location = requester.route("find.radar.EWR")
    .retrieveAndAwait<AirportLocation>()
```

如果使用[复合元数据](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md)（默认值），并且如果已注册的`Encoder`支持这些值，则可以添加额外的元数据值。例如：

Java

```
String securityToken = ... ;
ViewBox viewBox = ... ;
MimeType mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0");

Flux<AirportLocation> locations = requester.route("locate.radars.within")
        .metadata(securityToken, mimeType)
        .data(viewBox)
        .retrieveFlux(AirportLocation.class);
```

Kotlin

```
import org.springframework.messaging.rsocket.retrieveFlow

val requester: RSocketRequester = ...

val securityToken: String = ...
val viewBox: ViewBox = ...
val mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0")

val locations = requester.route("locate.radars.within")
        .metadata(securityToken, mimeType)
        .data(viewBox)
        .retrieveFlow<AirportLocation>()
```

对于`Fire-and-Forget`，使用`send()`方法，返回`Mono<Void>`。请注意，`Mono`仅表示消息已成功发送，而不表示消息已被处理。

对于`Metadata-Push`，使用带有`sendMetadata()`返回值的`Mono<Void>`方法。

### 5.3.附加注释的响应者

RSocket 响应器可以实现为`@MessageMapping`和`@ConnectMapping`方法。`@MessageMapping`方法处理单个请求，而`@ConnectMapping`方法处理连接级事件（设置和元数据推送）。带注释的响应器是对称支持的，用于从服务器端响应和从客户端响应。

#### 5.3.1.服务器响应者

要在服务器端使用带注释的响应器，将`RSocketMessageHandler`添加到你的 Spring 配置中，以检测`@Controller`带有`@MessageMapping`和`@ConnectMapping`的 bean 方法：

Java

```
@Configuration
static class ServerConfig {

    @Bean
    public RSocketMessageHandler rsocketMessageHandler() {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.routeMatcher(new PathPatternRouteMatcher());
        return handler;
    }
}
```

Kotlin

```
@Configuration
class ServerConfig {

    @Bean
    fun rsocketMessageHandler() = RSocketMessageHandler().apply {
        routeMatcher = PathPatternRouteMatcher()
    }
}
```

然后通过 Java RSocket API 启动一个 RSocket 服务器，并为响应者插入`RSocketMessageHandler`，如下所示：

Java

```
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);

CloseableChannel server =
    RSocketServer.create(handler.responder())
        .bind(TcpServerTransport.create("localhost", 7000))
        .block();
```

Kotlin

```
import org.springframework.beans.factory.getBean

val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()

val server = RSocketServer.create(handler.responder())
        .bind(TcpServerTransport.create("localhost", 7000))
        .awaitSingle()
```

`RSocketMessageHandler`默认支持[composite](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md)和[routing](https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md)元数据。如果需要切换到不同的 MIME 类型或注册其他元数据 MIME 类型，则可以设置其[MetadataExtractor](#rsocket-metadata-extractor)。

你需要设置元数据和数据格式所需支持的`Encoder`和`Decoder`实例。你可能需要`spring-web`模块来实现编解码。

默认情况下，`SimpleRouteMatcher`用于通过`AntPathMatcher`匹配路由。我们建议插入`spring-web`中的`PathPatternRouteMatcher`以进行有效的路线匹配。RSocket 路由可以是分层的，但不是 URL 路径。这两个路由匹配器都被配置为默认使用“.”作为分隔符，并且没有像 HTTP URL 那样的 URL 解码。

`RSocketMessageHandler`可以通过`RSocketStrategies`进行配置，如果你需要在同一进程中在客户机和服务器之间共享配置，这可能会很有用：

Java

```
@Configuration
static class ServerConfig {

    @Bean
    public RSocketMessageHandler rsocketMessageHandler() {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.setRSocketStrategies(rsocketStrategies());
        return handler;
    }

    @Bean
    public RSocketStrategies rsocketStrategies() {
        return RSocketStrategies.builder()
            .encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
            .decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
            .routeMatcher(new PathPatternRouteMatcher())
            .build();
    }
}
```

Kotlin

```
@Configuration
class ServerConfig {

    @Bean
    fun rsocketMessageHandler() = RSocketMessageHandler().apply {
        rSocketStrategies = rsocketStrategies()
    }

    @Bean
    fun rsocketStrategies() = RSocketStrategies.builder()
            .encoders { it.add(Jackson2CborEncoder()) }
            .decoders { it.add(Jackson2CborDecoder()) }
            .routeMatcher(PathPatternRouteMatcher())
            .build()
}
```

#### 5.3.2.客户响应者

需要在`RSocketRequester.Builder`中配置带有注释的客户端响应程序。详见[客户响应者](#rsocket-requester-client-responder)。

#### 5.3.3.@MessageMapping

一旦[server](#rsocket-annot-responders-server)或[client](#rsocket-annot-responders-client)响应者配置到位，`@MessageMapping`方法可按以下方式使用：

Java

```
@Controller
public class RadarsController {

    @MessageMapping("locate.radars.within")
    public Flux<AirportLocation> radars(MapRequest request) {
        // ...
    }
}
```

Kotlin

```
@Controller
class RadarsController {

    @MessageMapping("locate.radars.within")
    fun radars(request: MapRequest): Flow<AirportLocation> {
        // ...
    }
}
```

上面的`@MessageMapping`方法响应具有“locate.radars.within”路由的请求-流交互。它支持灵活的方法签名，可以选择使用以下方法参数：

|       Method Argument        |说明|
|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|          `@Payload`          |请求的有效载荷。这可以是异步类型的一个具体值，如`Mono`或`Flux`。<br/><br/>** 注：** 注释的使用是可选的。一个方法参数不是简单的类型<br/>，也不是任何其他受支持的参数，假定它是预期的有效负载。|
|      `RSocketRequester`      |向远端发出请求的请求者。|
|    `@DestinationVariable`    |基于映射模式中的变量从路由中提取的值，例如`@MessageMapping("find.radar.{id}")`。|
|          `@Header`           |按照[MetadataExtractor](#rsocket-metadata-extractor)中所述，为提取而注册的元数据值。|
|`@Headers Map<String, Object>`|按照[MetadataExtractor](#rsocket-metadata-extractor)中所述，为提取而注册的所有元数据值。|

返回值应该是一个或多个要序列化为响应有效负载的对象。这可以是异步类型，如`Mono`或`Flux`，具体值，或`void`或无值异步类型，如`Mono<Void>`。

`@MessageMapping`方法支持的 RSocket 交互类型是由输入的基数（即`@Payload`参数）的输出，其中基数表示如下：

|Cardinality|说明|
|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|     1     |要么是显式的值，要么是单值异步类型，如`Mono<T>`。|
|   Many    |一种多值异步类型，如`Flux<T>`。|
|     0     |对于输入，这意味着该方法没有`@Payload`参数。对于输出，这是<br/><br/>，这是`void`或无值异步类型，例如`Mono<Void>`。|

下表显示了所有输入和输出基数组合以及相应的交互类型：

|Input Cardinality|Output Cardinality|交互类型|
|-----------------|------------------|---------------------------------|
|      0, 1       |        0         |先开火后遗忘、请求-响应|
|      0, 1       |        1         |请求-响应|
|      0, 1       |       Many       |请求流|
|      Many       |    0, 1, Many    |请求通道|

#### 5.3.4.@ConnectMapping

`@ConnectMapping`处理 RSocket 连接开始时的`SETUP`框架，以及通过`METADATA_PUSH`框架的任何后续元数据推送通知，即`metadataPush(Payload)`中的`io.rsocket.RSocket`。

`@ConnectMapping`方法支持与[@MessageMapping](#rsocket-annot-messagemapping)相同的参数，但基于来自`SETUP`和`METADATA_PUSH`框架的元数据和数据。`@ConnectMapping`可以使用一种模式，将处理范围缩小到在元数据中具有路由的特定连接，或者如果没有声明任何模式，则所有连接都匹配。

`@ConnectMapping`方法不能返回数据，必须以`void`或`Mono<Void>`作为返回值进行声明。如果处理返回一个新连接的错误，那么该连接将被拒绝。在向`RSocketRequester`请求连接时，不能停止处理。详见[服务器请求者](#rsocket-requester-server)。

### 5.4.MetadataExtractor

响应者必须解释元数据。[复合元数据](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md)允许独立格式化的元数据值（例如用于路由、安全性、跟踪），每个值都具有自己的 MIME 类型。应用程序需要一种方法来配置元数据来支持 MIME 类型，以及一种方法来访问提取的值。

`MetadataExtractor`是一种契约，用于获取序列化的元数据并返回经过解码的名称-值对，然后可以通过名称像头一样访问这些对，例如通过注释处理程序方法中的`@Header`。

`DefaultMetadataExtractor`可以给出`Decoder`实例来解码元数据。开箱即用，它内置了对[“message/x.rsocket.routing.v0”](https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md)的支持，并将其解码为`String`，并保存在“route”键下。对于任何其他 MIME 类型，你需要提供`Decoder`，并按照以下方式注册 MIME 类型：

Java

```
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(fooMimeType, Foo.class, "foo");
```

Kotlin

```
import org.springframework.messaging.rsocket.metadataToExtract

val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Foo>(fooMimeType, "foo")
```

复合元数据可以很好地组合独立的元数据值。但是，请求者可能不支持复合元数据，或者可能选择不使用它。为此，`DefaultMetadataExtractor`可能需要自定义逻辑来将解码后的值映射到输出映射。下面是一个使用 JSON 进行元数据处理的示例：

Java

```
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(
    MimeType.valueOf("application/vnd.myapp.metadata+json"),
    new ParameterizedTypeReference<Map<String,String>>() {},
    (jsonMap, outputMap) -> {
        outputMap.putAll(jsonMap);
    });
```

Kotlin

```
import org.springframework.messaging.rsocket.metadataToExtract

val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Map<String, String>>(MimeType.valueOf("application/vnd.myapp.metadata+json")) { jsonMap, outputMap ->
    outputMap.putAll(jsonMap)
}
```

当通过`MetadataExtractor`配置`RSocketStrategies`时，你可以让`RSocketStrategies.Builder`使用已配置的解码器创建提取器，并只需使用回调来定制注册，如下所示：

Java

```
RSocketStrategies strategies = RSocketStrategies.builder()
    .metadataExtractorRegistry(registry -> {
        registry.metadataToExtract(fooMimeType, Foo.class, "foo");
        // ...
    })
    .build();
```

Kotlin

```
import org.springframework.messaging.rsocket.metadataToExtract

val strategies = RSocketStrategies.builder()
        .metadataExtractorRegistry { registry: MetadataExtractorRegistry ->
            registry.metadataToExtract<Foo>(fooMimeType, "foo")
            // ...
        }
        .build()
```

## 6. 反应库

`spring-webflux`依赖于`reactor-core`，并在内部使用它来组成异步逻辑并提供反应流支持。通常，WebFlux API 返回`Flux`或`Mono`（因为这些 API 是内部使用的），并宽容地接受任何反应流`Publisher`实现作为输入。使用`Flux`对`Mono`是很重要的，因为它有助于表示基数——例如，预期是单个还是多个异步值，这对于做出决策（例如，在编码或解码 HTTP 消息时）是必不可少的。

对于带注释的控制器，WebFlux 透明地适应应用程序选择的反应库。这是在[`ReactiveAdapterRegistry`]（https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/reactiveadapterregistry.html）的帮助下完成的。注册中心内置了对 RXJava3、 Kotlin 协程和 SmallRye Mutiny 的支持，但你也可以注册其他第三方适配器。

|   |在 Spring Framework5.3.11 中，对 RXJava1 和 2 的支持是不受欢迎的，下面是<br/>RXJava 自己的 EOL 建议和对 RXJava3 的升级建议。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------|

对于功能 API（例如[功能端点](#webflux-fn)、`WebClient`和其他），WebFlux API 的一般规则适用于-`Flux`和`Mono`作为返回值，并将反应流`Publisher`作为输入。当`Publisher`（无论是自定义的还是来自另一个反应库）被提供时，它只能被视为具有未知语义（0..n）的流。但是，如果语义是已知的，则可以用`Flux`或`Mono.from(Publisher)`来包装它，而不是传递 RAW`Publisher`。

例如，给定一个不是`Mono`的`Publisher`，JacksonJSON 消息编写器需要多个值。如果媒体类型意味着一个无限的流（例如，`application/json+stream`），则值是单独写入和刷新的。否则，值将被缓冲到列表中，并呈现为 JSON 数组。
