文档的介绍了基于 Reactive Streams API 构建的响应式 Web 应用程序的支持,该应用程序可在非阻塞服务器(例如 Netty,Undertow 和 Servlet 3.1+ 容器)上运行. 各个章节涵盖了 Spring WebFlux 框架,响应式 WebClient,对 测试的支持以及 reactive libraries. 对于 Servlet 堆栈 Web 应用程序,请参阅 Web on Servlet Stack.

1. Spring WebFlux

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

这两个 Web 框架的模块 (spring-webmvcspring-webflux),都在 Spring Framework 中. 每个模块都是可选的. 两种框架可以任选一个,也可以并存使用.例如,带有响应式 WebClient 的 Spring MVC 控制器.

1.1. 概述

为什么创建 Spring WebFlux?

部分原因是我们需要使用较少的线程和硬件资源来处理高并发非阻塞 I/O 请求. Servlet 3.1 确实提供了用于非阻塞 I/O 的 API.但是,使用它会导致 Servlet API 的其余部分偏离,在这些 API 中,(Filter, Servlet) 是同步的 ,(getParameter, getPart) 是阻塞的.这是促使新的通用 API 成为所有非阻塞运行时的基础的动机,更重要的原因是一些服务框架 比如 Netty 已经完美实现了异步和 non-blocking 服务开发方式.

另一个原因是函数式编程.除了在 Java5 提供的注解编程外(例如带注解的 REST 控制器或单元测试),Java 8 提供 lambda 函数编程 API.这对于非阻塞的应用程序和延续样式的 API (如由 CompletableFutureReactiveX 流式的 API)是有利的, 这些 API 允许以声明方式构成异步逻辑.在编程模型级别,Java 8 使 Spring WebFlux 能够与带注解的控制器一起提供功能性的 Web 端点.

1.1.1. 定义 “Reactive”

我们谈到了 “non-blocking” 和 “functional” ,但是 reactive 意味着什么?

术语 “reactive,” 是基于事件响应的编程模型,事件包括 网络 I/O 事件、页面的鼠标点击事件等等.也就是说非阻塞编程就是 Reactive,因为相对阻塞式编程而言,非阻塞编程以响应事件的通知为主,这些事件通知包括系统操作的状态变化和数据的状态变化.

我们 Spring 研发组考虑到一个非常重要的机制,那就是在 non-blocking 情况下的限流控制(又称 背压)问题.在同步调用情况下,阻塞调用是背压的自然形式,迫使调用者等待.在非阻塞代码中,去控制响应事件的速率就变得非常重要,可以防止生产者生产过快而导致消费者跟不上.

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

常见问题: 如果 publisher 太快怎么办?
Reactive Streams 的目的仅仅是建立机制和边界. 如果 publisher 无法放慢速度,那就让它 buffer 缓存,丢弃消息 甚至失败.

1.1.2. Reactive API

Reactive Streams 对于交互设计中扮演着一个非常重要的角色.但是由于它太底层了,对于编写应用层 API 没有太大作用.编写一个应用需要更高级的,功能更丰富,且具有函数编程特性的 API.这与 Java 8 Stream API 相似,但不仅适用于集合.这就是 reactive 库的作用.

Reactor 是 Spring WebFlux 的首选 reactive 库.它提供了 MonoFlux API 类型, ReactiveX vocabulary of operators 提供了非常丰富的操作 API 来处理 0..1 (Mono) 和 0..N (Flux) 的数据序列. Reactor 是 Reactive Streams 库,因此,它的所有运算符都支持无阻塞背压. Reactor 非常注重服务器端 Java.它是与 Spring 紧密合作开发的.

WebFlux 需要 Reactor 作为核心依赖,同时又可以与其他符合 Reactive Streams 规范的代码库结合使用.通常,WebFlux API 接受普通的 Publisher 作为输入,在内部将其适应于 Reactor 类型,使用它,然后返回 Flux 或 Mono 作为输出. 因此,您可以将任何 Publisher 作为输入传递,并且可以对输出应用操作,但是您需要调整输出以与其他 reactive 库一起使用.只要可行(例如,带注解的控制器),WebFlux 就会透明地适应 RxJava 或其他响应式库的使用.有关更多详细信息,请参见 Reactive 库 .

除了 Reactive API 外,WebFlux 还可以与 Kotlin 中的 Coroutines API 一起使用,从而提供了更强的编程风格.

1.1.3. 编程模型

spring-web 模块包含 Spring WebFlux reactive 基础,包括 HTTP 抽象,用于支持的服务器的 Reactive Streams adapters,codecs,以及与 Servlet API 相似但具有非阻塞的核心 WebHandler API .

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

  • 注解控制器: 与 Spring MVC 一致,并且使用和 spring-web 模块的相同注解. Spring MVC 和 WebFlux 控制器都支持 响应式(Reactor 和 RxJava)返回类型,因此,区分它们并不容易. 一个显着的区别是 WebFlux 还支持响应式 @RequestBody 参数.

  • 功能端点: 基于 Lambda 的轻量级功能编程模型. 你可将函数式编程看做是一个小的代码库,或者是可以路由和处理请求的一组应用套件.这与基于注解的编程最大的区别是应用程序从头到尾负责请求处理,而不通过注解来声明调用方法.

1.1.4. 适用性

Spring MVC 或 WebFlux?

这是个很平常的问题,但却还是有歧义的.实际上,两者可以相互辅助来解决更大范围的问题. 两者的设计旨在实现彼此的连续性和一致性,你可以同时使用两个框架的功能,每个框架来处理自己擅长的问题,这对双方都是有益的.下图展示了两者之间的关系,并列出了两者共同点和差异.

spring mvc and webflux venn

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

  • 如果您有运行正常的 Spring MVC 应用程序,则无需更改. 命令式编程是编写,理解和调试代码的最简单方法.因为从历史上看,大多数代码库都是阻塞的,你可以选择更多的库,结合到现有的框架中.

  • 如果你已经有一个非阻塞的 web 技术栈,那么你可以考虑使用 Spring WebFlux 作为替代方案,它可以提供同样的功能,同时 Spring WebFlux 提供了多种服务端容器支持(Netty,Tomcat,Jetty,Undertow 和 Servlet 3.1+ 容器),注解和函数式编程,以及与其他 Reactor 代码库的集成(Reactor, RxJava 等等)

  • 如果您对与 Java 8 lambda 或 Kotlin 提供的轻量级函数式编程比较感兴趣,你可以考虑使用 Spring WebFlux .也可以使用 Spring WebFlux 作为 web 服务的站点. 对于要求较低复杂性的较小应用程序或微服务(可以受益于更高的透明度和控制)而言,这也是一个不错的选择.

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

  • 评估应用程序技术上扩展性的一个简单方法是检查它的依赖那些组件,如果您要使用阻塞性 api (JPA、JDBC)或网络api,那么 Spring MVC 至少是系统架构上的最佳选择.从技术上讲,同时在单独的线程上使用 Reactor 和 RxJava 是可行的,但是你无法最大的利用非阻塞技术栈的优点.

  • 如果你已经有了一个 Spring MVC 应用程序,同时又需要远程调用其他的服务,这时可以尝试事件响应式客户端 WebClient. 通过 WebClient , 您可以直接从 Spring MVC 控制器方法返回响应式返回结果(Reactor,RxJava 或 or other). 并且每次远程调用的延迟或调用之间的相互依赖性越大,得到的好处就越大. Spring MVC控制器也可以通过 WebClient 调用其他支持事件响应的功能组件.

  • 如果您有庞大的团队,还要用 Spring WebFlux 技术栈,就必须要权衡陡峭的学习曲线和收益了.一个切实可行的方式就是使用 Spring WebFlux 部分技术栈 比如 WebClient,除此外,可以从小的地方着手使用 Spring WebFlux.我们认为对于绝大部分的应用程序,转变到基于事件响应式编程是不必要的.如果你还不确定使用响应式编程有哪些好处,可以首先了解下非阻塞 I/O 的工作原理及其效果(例如,单线程 Node.js 的并发性).

1.1.5. Servers

Tomcat,Jetty,Servlet 3.1+ 容器以及无服务器运行时(例如 Netty 和 Undertow)都支持 Spring WebFlux. 所有服务器都适应于底层 公共的 API,因此可以跨服务器支持更高级别的programming models.

Spring WebFlux 并没有提供内嵌的停止或者启动的服务的功能组件. 但是,从 Spring 配置和 WebFlux 基础结构 assemble 应用程序并用几行代码 run it它很容易.

Spring Boot 具有一个 WebFlux starter 组件,可以自动执行这些步骤.默认使用 Netty,但是通过更改 Maven 或 Gradle 依赖,可以轻松切换到 Tomcat,Jetty 或 Undertow. Spring Boot 默认为 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 (通常是非阻塞服务器)中,假定应用程序不阻塞,因此,非阻塞服务器使用一个小的、固定大小的线程池(event loop workers)来处理请求.

“可扩展” 和 “少量线程” 可能听起来是矛盾的,但是由于不会阻塞当前线程(而是依赖回调)意味着你不需要额外的线程来预防阻塞的情况出现.
Invoking a Blocking API(阻塞式 API)

如果确实需要使用阻塞怎么办? Reactor 和 RxJava 都提供了 publishOn 操作函数来控制线程的执行.这意味着在这种编程模型上开了一个口子.但是要记住,阻塞 api 并不适合这种并发模型.

Mutable State(可变状态)

在 Reactor 和 RxJava 中,您可以通过操作符声明逻辑,并且在运行时提供一个响应式管道,数据在管道中可以在不同的阶段按都顺序进行处理.这样做的一个好处是,它使应用程序不必关心数据状态的变化,因为管道中的应用程序代码永远不会被其他应用程序并发调用.

Threading Model(线程模型)

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

  • 在"普通的" Spring WebFlux 服务器上(例如,没有数据访问,也没有其他可选的依赖),你可以设置一个线程用于服务器端接收请求,设置几个线程用于处理请求,通常处理请求的线程数与 CPU 内核的数量一样多.但是,Servlet 容器可以设置更多的线程(例如,Tomcat 上的 10 个线程),以支持阻塞式 I/O 的 servlet 和非阻塞式 I/O 的 Servlet 3.1.

  • 响应式的 WebClient 采用 event-loop 方式.因此,你可以看到与之相关的少量固定数量的处理线程(例如, Netty 连接器中的 reactor-http-nio).. 但是,如果客户端和服务器都使用 Reactor Netty,则默认情况下,这两个共享 event-loop 资源.

  • Reactor 和 RxJava 都提供抽象线程池,称为调度器,以与 publishOn 运算符配合使用,该运算符用于将处理切换到其他线程池. 调度器都有一种特定的并发策略-例如, “parallel”(针对有限线程数量的cpu密集型程序)或 “elastic”(针对大量线程的 I/O 密集型程序). 如果您看到这样的线程,这意味着某些代码正在使用特定的线程池调度器策略.

  • 允许数据访问库和其他第三方依赖包也可以创建和使用它们自己的线程.

配置

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

1.2. Reactive Core

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

  • 在服务端请求处理过程中,提供了两个层次的支持,

    • HttpHandler: 提供了用于非阻塞 I/O 和响应式流背压机制的处理机制.以及用于响应式 Netty、Undertow、Tomcat、Jetty 和任何 Servlet 3.1+ 容器的适配器.

    • WebHandler API: 用于处理请求的高级通用 web API,使用这个 API 可以构建具体的编程模型,如带注解的控制器和功能入口.

  • 对于客户端,有一个基本的 ClientHttpConnector 来执行带有非阻塞 I/O 和响应式流背压机制的 HTTP 请求,以及用于 Reactor NettyJetty HttpClientApache HttpComponents 的 API. 在实际的应用开发过程中,我们使用的更高层级的 WebClient 就是构建在这个基本的 ClientHttpConnector 之上.

  • 对于客户端和服务器, codecs 用于序列化和反序列化 HTTP 请求和响应内容.

1.2.1. HttpHandler

HttpHandler 是一个简单的契约或者编程规范,它是被故意设计成最小化的,且只有一个方法来处理请求和响应.其主要的、也是唯一的目的是对不同的 HTTP 服务器 api 进行最小的抽象.

下表描述了它支持的服务器 api:

Server name Server API used Reactive Streams support

Netty

Netty API

Reactor Netty

Undertow

Undertow API

spring-web: Undertow to Reactive Streams bridge

Tomcat

Servlet 3.1 non-blocking I/O; Tomcat API to read and write ByteBuffers vs byte[]

spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge

Jetty

Servlet 3.1 non-blocking I/O; Jetty API to write ByteBuffers vs byte[]

spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge

Servlet 3.1 container

Servlet 3.1 non-blocking I/O

spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge

下表描述了不同服务端容器的程序依赖 (也参见 支持的版本):

Server name Group id Artifact name

Reactor Netty

io.projectreactor.netty

reactor-netty

Undertow

io.undertow

undertow-core

Tomcat

org.apache.tomcat.embed

tomcat-embed-core

Jetty

org.eclipse.jetty

jetty-server, jetty-servlet

下面的代码片段显示如何在不同的服务端容器中使用 HttpHandler 适配器

Reactor Netty

Java
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

Java
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

Java
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

Java
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 中. 该类使用 ServletHttpHandlerAdapter 包装 HttpHandler 并将其注册为 Servlet.

1.2.2. WebHandler API

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

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

  • User session with attributes.

  • Request attributes.

  • 解析请求中的 LocalePrincipal.

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

  • multipart data 数据的抽象.

  • 更多..

特殊的 bean 类型

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

Bean name Bean type Count Description

<any>

WebExceptionHandler

0..N

提供对 WebFilter 实例链和目标 WebHandler 的异常的处理. 有关更多详细信息, 请参见Exceptions.

<any>

WebFilter

0..N

在其余的过滤器链和目标 WebHandler 之前或之后使用拦截器风格的逻辑, 有关更多详细信息, 请参考 Filters

webHandler

WebHandler

1

请求处理器.

webSessionManager

WebSessionManager

0..1

通过 ServerWebExchange 上的方法公开的 WebSession 实例的管理器. 默认为 DefaultWebSessionManager.

serverCodecConfigurer

ServerCodecConfigurer

0..1

用于访问 HttpMessageReader 实例解析表单数据和多部分数据, 然后通过 ServerWebExchange 方法暴露这些数据. 默认情况下, ServerCodecConfigurer.create().

localeContextResolver

LocaleContextResolver

0..1

通过 ServerWebExchange 上的方法暴露的 LocaleContext 解析程序. 默认为 AcceptHeaderLocaleContextResolver.

forwardedHeaderTransformer

ForwardedHeaderTransformer

0..1

对于处理转发的类型的请求头, 可以通过提取和删除它们或仅通过删除它们来进行. 默认情况下不使用.

表单数据

ServerWebExchange 使用以下方法访问表单数据:

Java
Mono<MultiValueMap<String, String>> getFormData();
Kotlin
suspend fun getFormData(): MultiValueMap<String, String>

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

Multipart Data

ServerWebExchange 使用以下方法访问 multipart data:

Java
Mono<MultiValueMap<String, Part>> getMultipartData();
Kotlin
suspend fun getMultipartData(): MultiValueMap<String, Part>

DefaultServerWebExchange 使用配置的 HttpMessageReader<MultiValueMap<String, Part>>multipart/form-data 内容解析为 MultiValueMap. 默认情况下,这是没有任何第三方依赖的 DefaultPartHttpMessageReader. 或者, 可以使用基于 Synchronoss NIO Multipart 库的 SynchronossPartHttpMessageReader. 两者都是通过 ServerCodecConfigurer bean 配置的 (请参阅 Web Handler API) .

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

Forwarded Headers

当请求通过代理(例如负载均衡)进行处理时,主机,端口和 scheme 可能会更改,从客户端的角度来看,要创建指向正确的主机,端口和 scheme 的 url 是一个挑战.

RFC 7239 定义了 Forwarded HTTP header ,代理可以使用原始请求的信息. 还有其他非标准 header,包括 X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, X-Forwarded-Ssl, 和 X-Forwarded-Prefix.

ForwardedHeaderTransformer 是一个组件,可根据转发的 header 修改请求的主机,端口和 scheme,然后删除这些 header. 您可以将其声明为名称为 forwardedHeaderTransformer 的 Bean,并对其进行检测 和使用.

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

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

1.2.3. Filters

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

CORS

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

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

1.2.4. Exceptions

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

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

Exception Handler Description

ResponseStatusExceptionHandler

通过将响应设置为异常的 HTTP 状态码, 提供对 ResponseStatusException 类型的异常的处理.

WebFluxResponseStatusExceptionHandler

ResponseStatusExceptionHandler 的扩展, 它也可以确定 @ResponseStatus 注解上的任何异常的 HTTP 状态码. 该处理程序在 WebFlux 配置 中声明.

1.2.5. Codecs(编解码器)

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

  • EncoderDecoder 用于独立于HTTP编码和解码内容.

  • HttpMessageReaderHttpMessageWriter 是对HTTP消息内容进行编码和解码.

  • 可以使用 EncoderHttpMessageWriter 包装 Encoder ,使它能在 Web 应用程序中使用.同样,可以使用 DecoderHttpMessageReader 来包装 Decoder.

  • DataBuffer 抽象了不同的字节缓冲区表示形式 (例如 Netty ByteBuf, java.nio.ByteBuffer, 等.) 请参见 Data Buffers and Codecs 部分中的数据缓冲区和编解码器.

spring-core 模块提供 byte[], ByteBuffer, DataBuffer, ResourceString 编码器和解码器实现. spring-web 模块提供了 Jackson JSON, Jackson Smile, JAXB2, Protocol Buffers 和其他编码器和解码器, 以及仅用于 Web 的 HTTP 消息读取器和写入器实现,用于表单数据,多部分内容,服务器发送事件等.

ClientCodecConfigurerServerCodecConfigurer 通常用于配置和自定义编解码器. 请参阅有关 HTTP 消息编解码器 的部分.

Jackson JSON

Jackson 支持 JSON 和二进制 JSON(Smile) .

Jackson2Decoder 的工作方式如下:

  • Jackson 以异步, 非阻塞的形式解析,将字节块流聚合到 TokenBuffer 的每个块中,每个代表 JSON 对象.

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

  • 当解码为一个简单的 publisher (例如 Mono) 时, 有一个 TokenBuffer.

  • 当解码为多个 publisher (例如 Flux), 一旦为完整格式的对象接收到足够的字节,每个 TokenBuffer 就会传递给 ObjectMapper. 输入内容可以是 JSON 数组,也可以是 line-delimited JSON, 例如 NDJSON, JSON Lines, 或 JSON Text Sequences.

Jackson2Encoder 的工作方式如下:

  • 对于单个 publisher (例如 Mono), 只需通过 ObjectMapper 对其进行序列化即可.

  • 对于具有 "application/json" 的多个 publisher, 默认情况下使用 Flux#collectToList() 将值收集起来,然后序列化结果集合.

  • 对于具有流媒体类型 application/x-ndjsonapplication/stream+x-jackson-smile 的多个 publisher, 请使用 line-delimited JSON 格式分别对每个值进行编码,写入和刷新.其他流媒体类型可能已在编码器中注册

  • 对于 SSE 将为每个事件调用 Jackson2Encoder 并刷新输出以确保交付没有延迟.

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

表单数据

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

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

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

Multipart

MultipartHttpMessageReaderMultipartHttpMessageWriter 支持对 "multipart/form-data" 内容进行解码和编码. 反过来,MultipartHttpMessageReader 委托 HttpMessageReader 进行实际解析为 Flux<Part> 然后将这些内容简单地收集到 MultiValueMap.

默认情况下,使用的是 DefaultPartHttpMessageReader,但是可以通过 ServerCodecConfigurer 改变默认行为.

有关 DefaultPartHttpMessageReader 的更多信息, 请参考 javadoc of DefaultPartHttpMessageReader.

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

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

限制

可以通过 DecoderHttpMessageReader 的实现来对 部分 buffer 或 全部的输入流进行配置,比如,可以限制内存中缓冲的最大字节数.在某些情况下,由于输入被汇总并表示为单个对象而发生缓冲, 例如,具有 @RequestBody byte[], x-www-form-urlencoded 数据的控制器方法,等等. 在分割输入流(例如,定界文本,JSON 对象流等)时,流处理也会发生缓冲. 对于那些流情况,该限制适用于与流中一个对象关联的字节数.

要配置缓冲区大小,您可以检查给定的 DecoderHttpMessageReader 是否暴露了 maxInMemorySize 属性,如果这样,则 Javadoc 将具有有关默认值的详细信息. 在服务端,ServerCodecConfigurer 通过默认编码解码器的 maxInMemorySize 属性, 提供了一个设置所有编码解码器的 single place. 在客户端,可以在 WebClient.Builder 中更改限制.

对于 Multipart parsing,maxInMemorySize 属性限制了非文件部分的大小. 对于文件部件,它确定将部件写入磁盘的阈值. 对于写入磁盘的文件部件,还有一个额外的 maxDiskUsagePerPart 属性可限制每个部件的磁盘空间量. 还有一个 maxParts 属性,用于限制多部分请求中的部分总数. 要在 WebFlux 中配置所有3个,您需要向 ServerCodecConfigurer 提供一个预先配置的 MultipartHttpMessageReader 实例.

Streaming

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

DataBuffer

DataBuffer 是 WebFlux 中字节缓冲区的表示形式. 该部分参考的 Spring Core 中 Data Buffers and Codecs 部分. 要理解的关键点是,在诸如 Netty 之类的某些服务器上,字节缓冲区是池化的,并且对引用计数进行计数,并且在使用时必须将其释放,以避免内存泄漏.

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

1.2.6. Logging

Spring WebFlux 中的 DEBUG 级别日志记录意图提供一个紧密的,最小化的并且对用户友好的. 它侧重于一遍又一遍有用的高价值信息,而其他信息则仅在调试特定问题时才有用.

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

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

Log Id

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

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

敏感数据

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

下面的示例说明如何针对服务器端请求执行此操作:

Java
@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)
    }
}

下面的示例说明如何针对客户端请求执行此操作:

Java
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()
Appenders

日志库 (例如 SLF4J 和 Log4J 2) 提供了避免阻塞的异步记录器. 尽管它们有其自身的缺点, 例如可能丢弃无法进行排队日志记录的消息, 但它们是当前在响应式, 非阻塞应用程序中使用的最佳可用选项.

Custom codecs

应用程序可以注册自定义编解码器以支持其他媒体类型,也可以注册默认编解码器不支持的特定行为.

开发人员表达的某些配置选项在默认编解码器上强制执行. 自定义编解码器可能希望有机会与这些首选项保持一致,例如 强制执行缓冲限制记录敏感数据.

下面的示例说明如何针对客户端请求执行此操作:

Java
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

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

DispatcherHandler 从 Spring 配置中发现所需的委托组件. 它还被设计为 Spring Bean 本身,并实现 ApplicationContextAware 来访问其运行的上下文. 如果以 WebHandler 的 bean 名称声明了 DispatcherHandler,则 WebHttpHandlerBuilder 会发现它,而 WebHttpHandlerBuilder 会按照 WebHandler API 中的描述将请求处理链组合在一起.

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

将配置提供给 WebHttpHandlerBuilder 以构建处理链,如以下示例所示:

Java
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
Kotlin
val context: ApplicationContext = ...
val handler = WebHttpHandlerBuilder.applicationContext(context).build()

生成的 HttpHandler 已准备好与 server adapter 一起使用.

1.3.1. 特殊的 Bean 类型

DispatcherHandler 委托特殊bean处理请求并渲染视图. "special beans” 是指实现 WebFlux 框架的 Spring 管理的 Object 实例. 这些通常带有内置联系,但您可以自定义其属性并扩展或替换它们.

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

Bean type Explanation

HandlerMapping

将请求映射到处理程序. 其映射规则基于某些标准,其细节因 HandlerMapping 实现而异. HandlerMapping 的主要实现是 RequestMappingHandlerMapping(它支持带 @RequestMapping 注解的方法) 和 SimpleUrlHandlerMapping (它维护对处理程序的 URI 路径模式的显式注册) .

HandlerAdapter

无论实际调用处理程序如何,都可以帮助 DispatcherServlet 调用映射到请求的处理程序. 例如,调用带有注解的控制器,需要从注解中解析一些信息. HandlerAdapter 的主要目的是保护 DispatcherServlet 不受此类细节的影响.

HandlerResultHandler

处理来自处理程序调用的结果, 并最终确定响应. 请参阅 结果处理.

1.3.2. WebFlux 配置

应用程序可以声明处理请求所需的基础结构 bean(在 Web Handler APIDispatcherHandler 下列出).

在大多数情况下,WebFlux 配置 是最佳起点. 它声明了所需的 bean,并提供更高级别的配置回调 API 来自定义它.

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

1.3.3. 处理

DispatcherHandler 处理请求过程:

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

  • 如果找到处理程序,则通过适当的 HandlerAdapter 执行该处理程序,该适配器将执行返回的值暴露为 HandlerResult.

  • 通过直接写入响应或使用视图渲染,将 HandlerResult 提供给适当的 HandlerResultHandler 以完成处理.

1.3.4. 结果处理

通过 HandlerAdapter 调用处理程序的返回值连同其他一些上下文一起包装为 HandlerResult,并传递给第一个要求其支持的 HandlerResultHandler. 下表显示了可用的 HandlerResultHandler 实现,所有实现都在 WebFlux 配置 中声明:

Result Handler Type Return Values Default Order

ResponseEntityResultHandler

ResponseEntity, 通常来自 @Controller 实例.

0

ServerResponseResultHandler

ServerResponse, 通常来自功能端点.

0

ResponseBodyResultHandler

处理 @ResponseBody 方法 或 @RestController 类的返回值.

100

ViewResolutionResultHandler

CharSequence, View, Model, Map, Rendering, 或其他任何一个 Object 被视为一个 model attribute.

See also 视图解析.

Integer.MAX_VALUE

1.3.5. 异常

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

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

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

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

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

另请参见 “Annotated Controller” 部分中的 异常Exceptions 部分中的"异常”.

1.3.6. 视图解析

视图解析使您可以使用 HTML 模板和数据模型渲染浏览器,而无需将您与特定的视图技术联系在一起. 在 Spring WebFlux 中,通过专用的 HandlerResultHandler 支持视图解析,该 HandlerResultHandler 使用 ViewResolver 实例将 String (代表逻辑视图名称)映射到 View 实例. 然后使用 View 渲染视图.

Handling

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

  • String, CharSequence: 通过配置的 ViewResolver 实现列表解析为 View 的逻辑视图名称.

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

  • Rendering: 用于视图解析方案的API. 通过代码完成探索IDE中的选项.

  • Model, Map: 要添加到请求模型的额外模型属性.

  • 任何其他返回值(由 BeanUtils#isSimpleProperty) 确定的简单类型除外)都将被视为要添加到模型的模型属性. 属性名称是通过使用 约定从类名称继承的,除非存在处理程序方法 @ModelAttribute 注解.

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

配置视图解析就像将一个 ViewResolutionResultHandler bean 添加到您的 Spring 配置中一样简单. WebFlux Config 提供了专用于视图解析的配置 API.

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

重定向

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

控制器本身可以根据逻辑视图名称进行操作. 逻辑视图名称(例如 redirect:/myapp/some/resource ) 相对于当前 Servlet 上下文重定向,而名称如 redirect:http://myhost.com/some/arbitrary/path 重定向到绝对 URL.

内容协商

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

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

1.4. 注解控制器

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

在前面的示例中,该方法接受 Model 并将视图名称作为 String 返回,但是存在许多其他选项,本章稍后将对其进行说明.

1.4.1. @Controller

您可以使用标准的 Spring bean 定义来定义控制器 bean. @Controller 模板允许自动检测, 与 Spring 支持检测类路径中的 @Component 类一样,并会自动注册 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 是一个 组合注解,它本身由 @Controller@ResponseBody 元注解组成. 其每个方法都继承类型级别(type-level) 的 @ResponseBody 注解,因此,直接写入响应主体与视图渲染和使用 HTML 模板.

1.4.2. Request Mapping

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

还有 @RequestMapping 的 HTTP 方法特定的缩写变量:

  • @GetMapping

  • @PostMapping

  • @PutMapping

  • @DeleteMapping

  • @PatchMapping

这些简洁的注解是自定义注解,因为,大多数的控制器方法应该映射到 HTTP 方法而不是使用 @RequestMapping. 默认情况下, @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 模式

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

Pattern Description Example

?

匹配一个字符

"/pages/t?st.html" matches "/pages/test.html" and "/pages/t3st.html"

*

匹配路径段一个或多个字符

"/resources/*.png" matches "/resources/file.png"

"/projects/*/versions" matches "/projects/spring/versions" but does not match "/projects/spring/boot/versions"

**

匹配 0 个或多个路径

"/resources/**" matches "/resources/file.png" and "/resources/images/file.png"

"/resources/**/file.png" is invalid as ** is only allowed at the end of the path.

{name}

将匹配到的路径命名为 "name" 的变量

"/projects/{project}/versions" matches "/projects/spring/versions" and captures project=spring

{name:[a-z]+}

匹配路径正则表达式 "[a-z]+" 并将其命名为 "name" 变量

"/projects/{project:[a-z]+}/versions" matches "/projects/spring/versions" but not "/projects/spring1/versions"

{*path}

匹配零个或多个路径,直到路径结尾,并将其命名为 "path" 的变量

"/resources/{*file}" matches "/resources/images/file.png" and captures 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 等) ,您也可以注册对任何其他数据类型的支持. 请参见 类型转换DataBinder.

你可以显示命名 URI 变量(例如, @PathVariable("customId") ),但是如果名称是相同的,并且代码是使用调试信息编译的,或者在 Java 8 中使用 -parameters 编译器标记. 则可以保留该详细信息.

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

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

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 使用 PathPatternPathPatternParser 获得 URI 路径匹配支持. 这两个类都位于 spring-web 中,并且专门设计用于在运行时匹配大量 URI 路径模式的 Web 应用程序中的 HTTP URL 路径.

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

模式比较

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

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

默认映射模式(/**, {*varName})从评分中排除,并始终排在最后. 如果两种模式都适用,则选择较长的模式.

消费者媒体类型

您可以根据请求的 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_VALUEAPPLICATION_XML_VALUE.
生产者媒体类型

您可以根据 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 属性. 但是,与大多数其他请求映射属性不同,在类级别使用时,方法级别会生成属性覆盖,而不是扩展类级别声明.

MediaType 为常用媒体类型提供常量,例如 APPLICATION_JSON_UTF8_VALUEAPPLICATION_XML_VALUE.
参数, 请求头

您可以根据请求参数条件缩小请求映射. 您可以测试是否存在请求参数( 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 HEAD, OPTIONS

@GetMapping (和 @RequestMapping(method=HttpMethod.GET))一样,为了请求映射的目的,透明地支持 HTTP HEAD 以进行请求映射. 控制器方法无需更改. 在 javax.servlet.http.HttpServlet 中应用的响应包确保有 Content-Length 头并且设置为写入的字节数,但实际上不会写入响应.

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

对于没有 HTTP 方法声明的 @RequestMapping,Allow 请求头可以设置为 GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS. 控制器方法应始终声明支持的 HTTP 方法(例如,通过使用特定于 HTTP 方法的变体: @GetMapping, @PostMapping 等) .

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

自定义注解

Spring WebFlux 支持使用 组合注解进行请求映射. 这些注解本身是使用 @RequestMapping 进行元注解的,并且用于重新声明具有更窄,更具体目的的 @RequestMapping 属性的子集(或全部) .

@GetMapping, @PostMapping, @PutMapping, @DeleteMapping, 和 @PatchMapping 就是组合注解最好的示例, 提供它们是因为. 可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用 @RequestMapping,默认情况下,它与所有 HTTP 方法匹配. 如果您需要组合注解的示例,请查看如何声明这些注解.

Spring WebFlux 还支持使用自定义请求匹配逻辑的自定义请求映射属性. 这是一个更高级的选项,需要继承 RequestMappingHandlerMapping 并覆盖 getCustomMethodCondition 方法, 您可以在其中检查自定义属性并返回自己的 RequestCondition.

显式注册

您可以以编程方式注册处理程序方法,您可以将其用于动态注册或高级情况,例如不同 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 获取处理程序方法
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 获取处理程序方法
4 添加注册

1.4.3. 程序处理方法

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

方法参数

下表显示了受支持的控制器方法参数,任何参数都不支持响应式(Reactive)类型.

需要解析 I/O (例如,读取请求正文)的自变量支持响应式类型(Reactor,RxJava 或 其他). 这在"描述”列中进行了标记. 不需要阻塞的参数不应使用响应类型.

JDK 8 java.util.Optional 作为方法参数来支持的,它与具有必需属性的注解(例如 @RequestParam, @RequestHeader 等相结合). 并且等同于 required=false.

Controller method argument Description

ServerWebExchange

访问完整的 ServerWebExchange — 包含 HTTP 请求和响应, request 和 session 属性, checkNotModified 方法等等.

ServerHttpRequest, ServerHttpResponse

访问 HTTP 请求或响应.

WebSession

访问 session. 除非添加了属性, 否则不会强制打开一个新的会话. 支持 reactive 类型.

java.security.Principal

当前经过身份验证的用户 — 可能是 Principal 的实现类. 支持 reactive 类型.

org.springframework.http.HttpMethod

请求的 HTTP 方法.

java.util.Locale

当前请求的语言环境, 具体取决于可用的 LocaleResolver — 实际上是配置的 LocaleResolver/LocaleContextResolver.

java.util.TimeZone + java.time.ZoneId

与当前请求关联的时区, 由 LocaleContextResolver 决定.

@PathVariable

访问 URI 模板变量. See URI 模式.

@MatrixVariable

用于访问 URI 路径段中的键值对. 请参阅 矩阵变量.

@RequestParam

用于访问 Servlet 请求参数. 参数值将转换为声明的方法参数类型. 请参阅 @RequestParam.

请注意, 使用 @RequestParam 是可选的 — 例如, 设置其属性. 请参阅此表后面的 "其他任何参数".

@RequestHeader

用于访问请求头. 请求头的值将转换为声明的方法参数类型. 请参阅 @RequestHeader.

@CookieValue

用于访问 cookie. Cookie 值将转换为声明的方法参数类型. 请参阅 @CookieValue.

@RequestBody

用于访问 HTTP 请求正文. 正文内容通过使用 HttpMessageReader 实例转换为声明的方法参数类型. 支持 reactive 类型. 请参阅 @RequestBody.

HttpEntity<B>

用于访问请求头和正文. 主体将通过 HttpMessageReader 实例进行转换. 支持 reactive 类型. 请参阅 HttpEntity.

@RequestPart

用于访问 multipart/form-data 请求中的数据. 支持 reactive 类型. 请参阅 Multipart ContentMultipart Data.

java.util.Map, org.springframework.ui.Model, and org.springframework.ui.ModelMap.

用于访问在 HTML 控制器中使用的模型, 并作为视图渲染的一部分暴露给模板.

@ModelAttribute

用于访问模型中的现有属性 (如果不存在, 则进行实例化) , 并应用数据绑定和验证. 见 @ModelAttribute 以及 ModelDataBinder.

请注意, 使用 @ModelAttribute 是可选的-例如, 设置其属性. 请参阅此表后面的 "其他任何参数"

Errors, BindingResult

用于访问命令对象 (即 @ModelAttribute 变量) 的验证和数据绑定中的错误 . ErrorsBindingResult 参数必须在验证方法参数后声明.

SessionStatus + class-level @SessionAttributes

为了标记表单处理完成, 将触发通过声明的类级别的 @SessionAttributes 注解清楚会话属性. 请参阅 @SessionAttributes 获取更多的细节.

UriComponentsBuilder

为当前请求的 host, port, scheme, 和 context path 准备 URL. 请参阅 URI 链接.

@SessionAttribute

访问任何 session 属性 — 与类级别 @SessionAttributes 注解声明而存储在会话中的模型属性相反. 请参阅 @SessionAttribute 获取更多的细节.

@RequestAttribute

访问 request 属性. 请参阅 @RequestAttribute 获取更多细节.

Any other argument

如果一个方法参数不匹配于任何上述的, 它是, 在默认情况下, 解析为 @RequestParam, 如果它是一个简单的类型, 如由 BeanUtils#isSimpleProperty, 否则,作为 @ModelAttribute.

返回值

下表描述了支持的控制器方法返回值.请注意,所有返回值通常都支持 Reactor,RxJava 其他 的库中的响应类型.

控制器方法返回值 描述

@ResponseBody

返回值通过 HttpMessageWriter 实现转换并写入响应. 请参阅 @ResponseBody.

HttpEntity<B>, ResponseEntity<B>

指定完整响应(包括 HTTP 头和主体) 的返回值将通过 HttpMessageWriter 实现转换并写入响应. 请参阅 ResponseEntity.

HttpHeaders

用于返回带头部信息且没有正文的响应.

String

要使用 ViewResolver 实现解析的视图名称,并与隐式模型一起使用 - 通过命令对象和 @ModelAttribute 方法确定. 处理程序方法还可以通过声明 Model 参数(如上所述) 以编程方式丰富模型

View

用于与隐式模型一起呈现的 View 实例 - 通过命令对象和 @ModelAttribute 方法确定. 处理程序方法还可以通过声明 Model 参数(如上所述)以编程方式丰富模型.

java.util.Map, org.springframework.ui.Model

要添加到隐式模型的属性,其中视图名称是根据请求路径隐式确定的.

@ModelAttribute

要添加到模型的属性, 视图名称根据请求路径隐式确定.

请注意 @ModelAttribute 是可选的. 参考此表后面的 “Any other return value”

Rendering

用于模型和视图渲染方案的 API.

void

具有 void 返回类型的方法可能是异步的(例如, Mono<Void>) 的返回类型, 或返回值为 null 的方法(如果它还具有 ServerHttpResponse,ServerWebExchange 参数或 @ResponseStatus 注解) 则认为已完全处理该响应. 如果控制器已进行正 ETag 或 lastModified 进行时间戳检查,则也是如此

如果以上条件都不成立, 则 void 返回类型还可以为 REST 控制器表示 "无响应正文", 或者为 HTML 控制器表示默认视图名称选择.

Flux<ServerSentEvent>, Observable<ServerSentEvent>, or other reactive type

Emit server-sent events. The ServerSentEvent wrapper can be omitted when only data needs to be written (however, text/event-stream must be requested or declared in the mapping through the produces attribute).

Any other return value

任何与此表中任何早期值不匹配且返回值为 Stringvoid 的返回值都被视为视图名称 , 前提是它不是简单类型,由 BeanUtils#isSimpleProperty, 确定,简单类型的值仍未解决.

类型转换

如果参数声明为 String 以外的其他参数,则表示某些带注解的控制器方法参数(例如 @RequestParam, @RequestHeader, @PathVariable, @MatrixVariable, 和 @CookieValue) 可能需要进行类型转换.

对于此类情况,将根据配置的转换器自动应用类型转换. 默认情况下,支持简单类型(int, long, Date 和其他) . 您可以通过 WebDataBinder(请参阅DataBinder) 或使用 FormattingConversionService 注册 Formatters 来自定义类型转换. 请参见 Spring Field Formatting.

类型转换中的一个实际问题是处理空的 String 值. 如果该值由于类型转换而变为 null, 则将其视为丢失. Long, UUID, 和其他目标类型. 如果要允许注入 null, 则可以在参数注解中使用 required 标志, 或将参数声明为 @Nullable.

矩阵变量

RFC 3986 讨论了路径段中的携带键值对. 在 Spring MVC 中,我们将那些基于 Tim Berners-Lee 的 “old post” 称为 "`matrix variables(矩阵变量) `" ,但它们也可以称为 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

您可以使用 @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 的 "请求参数” 概念将查询参数,表单数据和多部分合并为一个. 但是,在 WebFlux 中,每个服务器都可以通过 ServerWebExchange 单独访问. 虽然 @RequestParam 仅绑定到查询参数,但是您可以使用数据绑定将查询参数, 表单数据和多部分应用到command object.

若参数使用了该注解,则该参数默认是必须提供的.但您可以通过将 @RequestParam 注解的 required 属性设置为 false 或通过使用 java.util.Optional 包装器声明参数来指定方法参数是可选的.

如果目标方法参数类型不是 String,则会自动应用类型转换. 请参阅 类型转换.

@RequestParam 注解声明为 Map<String, String>MultiValueMap<String, String> 时, 如果注解中未指定参数名称,则会使用每个给定参数名称的请求参数值填充映射.

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

@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-EncodingKeep-Alive 头的值:

Java
@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, (1)
        @RequestHeader("Keep-Alive") long keepAlive) { (2)
    //...
}
1 获取 Accept-Encoding 头部信息
2 获取 Keep-Alive 头部信息.
Kotlin
@GetMapping("/demo")
fun handle(
        @RequestHeader("Accept-Encoding") encoding: String, (1)
        @RequestHeader("Keep-Alive") keepAlive: Long) { (2)
    //...
}
1 获取 Accept-Encoding 头部信息
2 获取 Keep-Alive 头部信息.

如果目标方法参数类型不是 String,则会自动应用类型转换. 请参阅类型转换.

Map<String, String>,MultiValueMap<String, String>HttpHeaders 参数上使用 @RequestHeader 注解时,将使用所有请求头值填充映射.

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

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

考虑使用以下 cookie 的请求:

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

以下示例显示了如何获取 cookie 值:

Java
@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,则会自动应用类型转换. 请参阅类型转换.

@ModelAttribute

您可以在方法参数上使用 @ModelAttribute 注解来从模型访问属性,或者如果不存在则将其实例化. model 属性还覆盖了名称与字段名称匹配的请求参数和表单字段的值. 这称为数据绑定,它使您不必处理解析和转换单个查询参数和表单字段. 以下示例显示了如何执行此操作:

Java
@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.

  • 它可能因为 @SessionAttributes 注解的使用已经存在在model中.

  • 它可能是调用了自身的默认构造器被实例化出来的.

  • 他可能从调用具有与请求参数或表单字段匹配的参数的 "primary constructor". 参数名称通过 JavaBeans @ConstructorProperties 或字节码中的运行时保留参数名称确定.

下一步就是数据的绑定,WebExchangeDataBinder 类能将请求参数,包括字符串的查询参数和表单字段等,通过名称匹配到 model 的属性上. 成功匹配的字段在需要的时候会进行一次类型转换(从 String 类型到目标字段的类型) ,然后被填充到model对应的属性中, 有关数据绑定(和验证) 的更多信息,请参阅Validation. 有关自定义数据绑定的更多信息,请参阅DataBinder.

数据绑定可能导致错误. 默认情况下,会引发 WebExchangeBindException . 但是,要在控制器方法中检查此类错误,可以在 @ModelAttribute 旁边添加一个 BindingResult 参数,如以下示例所示:

Java
@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 ValidationSpring validation) ,您可以在数据绑定后自动应用验证. 以下示例显示了如何执行此操作:

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}
1 验证 Pet 实例.
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
    if (result.hasErrors()) {
        return "petForm"
    }
    // ...
}
1 验证 Pet 实例.

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

Java
@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) 确定 并且未被任何其他参数解析器解析,都被视为使用 @ModelAttribute 进行注解.

@SessionAttributes

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

以下示例使用 @SessionAttributes 注解:

Java
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
    // ...
}
1 使用 @SessionAttributes 注解.
Kotlin
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
    // ...
}
1 使用 @SessionAttributes 注解.

在第一个请求中,当名称为 pet 的 model 属性添加到模型中时,他会自动保存到 WebSession 会话中,并保持不变,直到另一个控制器方法使用 SessionStatus 方法参数来清除存储,如下例所示:

Java
@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

如果需要访问已存在的被全局 session 属性,例如在控制器之外(如通过过滤器) 的(可有可无) ,请在方法参数上使用 @SessionAttribute 注解:

Java
@GetMapping("/")
public String handle(@SessionAttribute User user) { (1)
    // ...
}
1 使用 @SessionAttribute.
Kotlin
@GetMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
    // ...
}
1 使用 @SessionAttribute.

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

作为控制器工作流的一部分,在会话中临时存储模型属性的方法可以使用 @SessionAttributes,详情请参阅 @SessionAttributes

@RequestAttribute

@SessionAttribute 类似,@RequestAttribute 注解可用于访问由过滤器( WebFilter )创建的已存在的请求属性:

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

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

Java
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 访问各个部分,如以下示例所示:

Java
@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 获取文件

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

Java
@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 获取元数据

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

Java
@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,如以下示例所示:

Java
@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 使用 @RequestBody.

要以流方式顺序访问多部分数据,可以将 @RequestBodyFlux<Part>(或 Kotlin 中的 Flow<Part>)一起使用,如以下示例所示:

Java
@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) { (1)
    // ...
}
1 使用 @RequestBody.
Kotlin
@PostMapping("/")
fun handle(@RequestBody parts: Flow<Part>): String { (1)
    // ...
}
1 使用 @RequestBody.
@RequestBody

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

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

与 Spring MVC 不同,在 WebFlux 中,@RequestBody 方法的参数支持响应类型. 以及完全无阻塞的阅读和(客户端到服务器)流式传输.

Java
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
    // ...
}
Kotlin
@PostMapping("/accounts")
fun handle(@RequestBody accounts: Flow<Account>) {
    // ...
}

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

您可以将 @RequestBodyjavax.validation.Valid 或 Spring 的 @Validated 注解结合使用,这两种注解都会导致应用标准 Bean 验证. 默认情况下,验证错误会导致 WebExchangeBindException,并将其转换为 400(BAD_REQUEST) 响应. 或者,您可以通过 BindingResult 参数在控制器内本地处理验证错误,如以下示例所示:

Java
@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

HttpEntity 与使用 @RequestBody 或多或少有些类似,但它基于一个暴露请求头和正文的容器对象. 以下清单显示了一个示例:

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

您可以在方法上使用 @ResponseBody 注解,以通过 HttpMessageWriter 将返回序列化到响应主体. 以下清单显示了一个示例:

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

类级别也支持 @ResponseBody ,在这种情况下,它由所有控制器方法继承. 例如 @RestController 的效果,它只不过是一个用 @Controller@ResponseBody 标记的元注解.

您可以将 @ResponseBody 与 reactive 类型一起使用. 这意味着您可以返回 Reactor 或 RxJava 类型,并将它们产生的异步值呈现给响应.有关更多详细信息,请参阅StreamingJSON rendering.

您可以将 @ResponseBody 方法与 JSON 序列化视图结合使用. 有关详细信息,请参阅 Jackson JSON .

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

ResponseEntity

ResponseEntity@ResponseBody 类似,但具有状态和响应头. 例如:

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

WebFlux 支持使用单值reactive type异步生成 ResponseEntity,and/or 主体的单值和多值 reactive 类型.

这允许使用 ResponseEntity 进行各种异步响应, 如下所示:

  • ResponseEntity<Mono<T>>ResponseEntity<Flux<T>> 可以立即获得响应状态和响应头,而在稍后以异步方式提供正文时. 如果主体包含 0..1 个值, 请使用 Mono,如果主体可以产生多个值,请使用 Flux.

  • Mono<ResponseEntity<T>> 在稍后的时间异步提供所有的 — response status, headers, 和 body. 这允许响应状态和响应头根据异步请求处理的结果而变化.

  • Mono<ResponseEntity<Mono<T>>>Mono<ResponseEntity<Flux<T>>> 是另一种可能的方法, 但是不太常见. 它们首先异步地提供响应状态和响应头, 然后第二次异步地提供响应主体.

Jackson JSON

Spring 为 Jackson JSON 库提供支持.

JSON 序列化视图

Spring WebFlux 为 Jackson的序列化视图提供内置支持,允许仅渲染 Object 中所有字段的子集. 为了与 @ResponseBody 控制器方法或者返回 ResponseEntity 的控制器方法一起使用,可以简单地将 @JsonView 注解放在参数上,指定需要使用的视图类或接口即可. 如以下示例所示:

Java
@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
}
@JsonView 允许一组视图类,但每个控制器方法只能指定一个. 如果需要激活多个视图,可以使用复合接口.

1.4.4. Model

您可以使用 @ModelAttribute 注解:

  • @RequestMapping 方法中的方法参数,用于从 model 创建或访问 Object 并通过 WebDataBinder 将其绑定到请求.

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

  • @RequestMapping 方法上标记其返回值是一个模型属性.

本节讨论 @ModelAttribute 注解可被应用在方法或方法参数上 - 前面列表中的第二项. 控制器可以包含任意数量的 @ModelAttribute 方法. 在同一控制器中的 @RequestMapping 方法之前调用所有这些方法. @ModelAttribute 方法也可以通过 @ControllerAdvice 在控制器之间共享. 有关更多详细信息,请参阅 Controller Advice 部分.

@ModelAttribute 方法具有灵活的方法签名. 除了与 @ModelAttribute 本身或请求体相关的任何内容外,它们支持许多与 @RequestMapping 方法相同的参数.

以下示例显示了 @ModelAttribute 方法:

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

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

Java
@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountRepository.findAccount(number);
}
Kotlin
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
    return accountRepository.findAccount(number);
}
如果未明确指定名称,框架将根据属性的类型给予一个默认名称,如 Conventions 的 javadoc 中所述. 你可以通过设置 @ModelAttribute 注解的值来改变默认值. 当向 Model 中直接添加属性时,请使用合适的重载方法 addAttribute.

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

Java
@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 方法的返回值将会被解释为 model 的一个属性,而非一个视图名. 此时视图名将以视图命名约定来方式来决议,与返回值为 void 的方法所采用的处理方法类似. @ModelAttribute 还可以自定义模型属性名称,如以下示例所示:

Java
@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

@Controller@ControllerAdvice 类可以使用 @InitBinder 方法初始化 WebDataBinder 的实例,而这些方法又可以:

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

  • 将基于字符串的请求值(例如请求参数,路径变量,请求头,cookie 等) 转换为目标类型的控制器方法参数.

  • 在呈现HTML表单时将模型对象值格式化为 String 值.

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

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

Java
@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))
    }

    // ...
}

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

Java
@Controller
public class FormController {

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

    // ...
}
1 添加一个自定义 formatter ( 本例中为 DateFormatter).
Kotlin
@Controller
class FormController {

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

    // ...
}
1 添加一个自定义 formatter ( 本例中为 DateFormatter).
Model Design

In the context of web applications, data binding involves the binding of HTTP request parameters (that is, form data or query parameters) to properties in a model object and its nested objects.

Only public properties following the JavaBeans naming conventions are exposed for data binding — for example, public String getFirstName() and public void setFirstName(String) methods for a firstName property.

在 web 应用的上下文中,data binding 涉及到 HTTP 请求参数(即表单数据或查询参数)到模型对象中的属性,以及 它的嵌套对象。

只有 public 属性遵循 JavaBeans 命名约定 并暴露为数据绑定参数 ——例如,public String getFirstName()firstName 属性的 public void setFirstName(String) 方法。

模型对象及其嵌套对象有时也称为 command objectform-backing objectPOJO(普通旧 Java 对象)。

默认情况下,Spring 允许绑定到 model object 中的所有公共属性。这意味着您需要仔细考虑 model 具有哪些公共属性,因为 客户端可以针对任何公共属性路径,包括一些您不期望的路径。

例如,给定一个 HTTP 表单数据端点,恶意客户端可以为模型对象中存在的属性,但是在 web 应用的上下文中,数据绑定 涉及到 HTTP 请求参数绑定 (即表单数据或查询参数)到模型对象中的属性,以及它的嵌套对象。

推荐的方法是使用仅暴露的 dedicated model object(专用模型对象) 与表单提交相关的属性。例如,在用于更改的表格上 用户的电子邮件地址,模型对象应声明一组最少的属性,例如 如下面的 ChangeEmailForm

public class ChangeEmailForm {

    private String oldEmailAddress;
    private String newEmailAddress;

    public void setOldEmailAddress(String oldEmailAddress) {
        this.oldEmailAddress = oldEmailAddress;
    }

    public String getOldEmailAddress() {
        return this.oldEmailAddress;
    }

    public void setNewEmailAddress(String newEmailAddress) {
        this.newEmailAddress = newEmailAddress;
    }

    public String getNewEmailAddress() {
        return this.newEmailAddress;
    }

}

如果您不能或不想为每个数据使用 专用模型对象 绑定用例,您 must 限制数据绑定允许的属性。 理想情况下,您可以通过注册 allowed field patterns 通过 WebDataBinder 上的 setAllowedFields() 方法。

例如,要在您的应用程序中注册允许的字段模式,您可以实现一个 @Controller@ControllerAdvice 组件中的 @InitBinder 方法,如下所示:

@Controller
public class ChangeEmailController {

    @InitBinder
    void initBinder(WebDataBinder binder) {
        binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
    }

    // @RequestMapping methods, etc.

}

除了注册允许的模式,还可以注册 disallowed field patterns 通过 DataBinder 及其子类中的 setDisallowedFields() 方法。 但是请注意,"allow list" 比 "deny list" 更安全。 最后,setAllowedFields() 应该优于 setDisallowedFields()

请注意,与允许的字段模式匹配是区分大小写的; 而,匹配 针对不允许的字段模式不区分大小写。 此外,一个匹配的字段不允许的模式将不会被接受,即使它也恰好匹配 允许列表。

正确配置允许和不允许的字段模式非常重要直接暴露您的 domain 模型以进行数据绑定时。 否则,它的安全隐患大。

此外,强烈建议您使用您域中的类型 JPA 或 Hibernate 实体等模型作为数据绑定场景中的模型对象。

1.4.6. 异常

@Controller@ControllerAdvice 可以使用 @ExceptionHandler 方法来处理来自控制器方法的异常,如下例所示:

Java
@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) 或顶级包装器中的异常(例如,包含在 IllegalStateException 内的 IOException) 相匹配.

对于匹配的异常类型,最好将目标异常声明为方法参数,如前面的示例所示. 或者,注解声明可以缩小要匹配的异常类型. 我们通常建议在参数签名中尽可能具体,并在以相应顺序优先的 @ControllerAdvice 上声明您的 root 异常映射. 有关详细信息,请参见 MVC 部分 .

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

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

REST API exceptions

REST 服务的一个常见要求是在响应正文中包含错误详细信息. Spring Framework 不会自动执行此操作,因为响应正文中的错误详细信息的表示是特定于应用程序的. 但是,@RestController 可以使用带有 ResponseEntity 返回值的 @ExceptionHandler 方法来设置响应的状态和正文. 这些方法也可以在 @ControllerAdvice 类中声明,以全局应用它们.

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

1.4.7. Controller Advice

通常,在 @Controller 类上声明 @ExceptionHandler, @InitBinder, 和 @ModelAttribute 注解. 如果您希望此类方法更全局地应用(跨控制器) ,则可以在标有 @ControllerAdvice@RestControllerAdvice 的类中声明它们.

@ControllerAdvice @Component 注解,这意味着可以通过组件扫描将这些类注册为 Spring bean. @RestControllerAdvice 也是一个用 @ControllerAdvice@ResponseBody 标记的元注解,这实际上意味着 @ExceptionHandler 方法通过消息转换(与视图解析或模板渲染相对) 呈现给响应主体.

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

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

Java
// 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 javadoc .

1.5. 功能端点

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

1.5.1. 概述

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

传入的请求通过 RouterFunction 路由到处理程序函数: 该函数接受 ServerRequest 并返回可选的 HandlerFunction (例如 Mono<HandlerFunction>) . 当路由器功能匹配时,返回处理程序功能. 否则为空的 Mono. RouterFunction 等效于 @RequestMapping 注解,但主要区别在于路由器功能不仅提供数据,还提供行为.

RouterFunctions.route() 提供了一个有助于构建路由器的路由器构建器,如以下示例所示:

Java
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 并通过内置 server adapters 之一进行安装:

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

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

1.5.2. HandlerFunction

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

ServerRequest

ServerRequest 提供对 HTTP 方法,URI,请求头和查询参数的访问,而通过 body 方法提供对主体的访问.

下面的示例将请求正文提取到 Mono<String>:

Java
Mono<String> string = request.bodyToMono(String.class);
Kotlin
val string = request.awaitBody<String>()

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

Java
Flux<Person> people = request.bodyToFlux(Person.class);
Kotlin
val people = request.bodyToFlow<Person>()

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

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

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

Java
Mono<MultiValueMap<String, String>> map = request.formData();
Kotlin
val map = request.awaitFormData()

下面的例子展示了如何访问多部分数据作为一个 map:

Java
Mono<MultiValueMap<String, Part>> map = request.multipartData();
Kotlin
val map = request.awaitMultipartData()

以下示例显示了如何以流方式一次访问多个部分:

Java
Flux<Part> parts = request.body(BodyExtractors.toParts());
Kotlin
val parts = request.body(BodyExtractors.toParts()).asFlow()
ServerResponse

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

Java
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 (CREATED) 响应:

Java
URI location = ...
ServerResponse.created(location).build();
Kotlin
val location: URI = ...
ServerResponse.created(location).build()

根据所使用的编解码器,可以传递提示参数以自定义主体的序列化或反序列化方式. 例如,要指定 Jackson JSON view:

Java
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
Kotlin
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
处理 Classes

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

Java
HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().bodyValue("Hello World");
Kotlin
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }

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

Java
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 是一个处理函数,它以JSON格式返回存储库中找到的所有 Person 对象.
2 createPerson 是一个处理函数,用于存储请求正文中包含的新 Person 请注意 PersonRepository.savePerson(Person) 返回 Mono<Void>: 一个空的 Mono ,当从请求中读取并存储此人时,它将发出完成信号. 因此,当接收到完成信号时(即,保存 Person 时),我们使用 build(Publisher<Void>) 方法发送响应.
3 getPerson 是一个处理程序函数,它返回由 id 路径变量标识的单个人. 我们从存储库中检索该 Person 并创建一个 JSON 响应(如果找到) . 如果未找到,我们使用 switchIfEmpty(Mono<T>) 返回404 Not 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 是一个处理函数,它以 JSON 格式返回存储库中找到的所有 Person 对象.
2 createPerson 是一个处理函数,用于存储请求正文中包含的新 Person. 请注意, PersonRepository.savePerson(Person) 是一个没有返回类型的函数.
3 getPerson 是一个处理程序函数,它返回由 id 路径变量标识的单个人. 我们从存储库中检索该 Person 并创建一个 JSON 响应(如果找到) . 如果未找到,我们将返回 404 Not Found 响应.
Validation

功能端点可以使用 Spring 的验证工具将验证应用于请求正文. 例如,给定 Person 的自定义 Spring Validator 实现:

Java
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 应用 validation.
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 应用 validation.
3 引发 400 响应的异常.

处理程序还可以通过基于 LocalValidatorFactoryBean 创建和注入全局 Validator 实例来使用标准 Bean 验证 API(JSR-303) . 请参阅Spring Validation.

1.5.3. RouterFunction

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

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

除了基于 HTTP 方法的映射外,路由构建器还提供了一种在映射到请求时引入其他断言的方法. 对于每个 HTTP 方法,都有一个重载的变体,它以 RequestPredicate 作为参数,尽管可以表达其他约束.

断言

您可以编写自己的 RequestPredicate,但是 RequestPredicates 实用程序类根据请求路径,HTTP 方法,内容类型等提供常用的实现. 以下示例使用请求断言基于 Accept 头创建约束:

Java
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 时, 这一点也很重要, 这将在后面进行描述. 请注意,此行为不同于基于注解的编程模型,在该模型中,将自动选择 "最特定" 的控制器方法.

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

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

  • RouterFunction.and(RouterFunction)

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

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

Java
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 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匹配的 Accept 头的 GET /person/{id} 被路由到 PersonHandler.getPerson
2 带有与JSON匹配的 Accept 头的 GET /person 被路由到 PersonHandler.listPeople
3 没有其他断言的 POST POST /person 被路由到 PersonHandler.createPerson
4 otherRoute 是在其他地方创建的路由器功能,并将其添加到构建的路由中.
嵌入路由

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

Java
RouterFunction<ServerResponse> route = route()
    .path("/person", builder -> builder (1)
        .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        .GET(accept(APPLICATION_JSON), handler::listPeople)
        .POST(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(handler::createPerson)
    }
}

尽管基于路径的嵌套是最常见的,但是您可以通过使用构建器上的 nest 方法来嵌套在任何种类的断言上. 上面的内容仍然包含一些以共享的 Accept-header 断言形式出现的重复. 通过将 nest 方法与 accept 一起使用,我们可以进一步改进:

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

1.5.4. 运行服务器

如何在HTTP服务器中运行路由器功能? 一个简单的选择是转换路由器,使用以下其中一种功能将其作用于 HttpHandler:

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

然后,可以通过遵循 HttpHandler 来获取特定于服务器的指令,将返回的 HttpHandler 与许多服务器适配器一起使用.

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

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

  • HandlerFunctionAdapter: 简单的适配器,使 DispatcherHandler 调用映射到请求的 HandlerFunction.

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

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

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

Java
@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. 过滤器处理程序功能

您可以使用路由功能构建器上的 before,afterfilter 方法来过滤处理程序函数. 使用注解,可以通过使用 @ControllerAdvice,ServletFilter 或同时使用两者来实现类似的功能. 该过滤器将应用于构建器构建的所有路由. 这意味着在嵌套路由中定义的过滤器不适用于 "top-level" 路由. 例如,考虑以下示例:

Java
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(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(handler::createPerson)
        after { _, response -> (2)
            logResponse(response)
        }
    }
}
1 添加自定义请求头的 before 过滤器仅应用于两个GET路由.
2 记录响应的 after 过滤器将应用于所有路由,包括嵌套路由.

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

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

Java
SecurityManager securityManager = ...

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

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

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

通过专用的 CorsWebFilter. 提供对功能端点的 CORS 支持.

1.6. URI 链接

本部分介绍了 Spring 框架中可用于 URI 的各种选项.

1.6.1. UriComponents

Spring MVC 和 Spring WebFlux

UriComponentsBuilder 有助于从 URI 模板变量构建 URI. 如下例所示:

Java
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 缩短,如下例所示:

Java
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(这意味着编码) 来进一步缩短它,如下例所示:

Java
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 模板进一步缩短它,如下例所示:

Java
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 and Spring WebFlux

UriComponentsBuilder 实现了 UriBuilder. 您可以使用 UriBuilderFactory 创建一个 UriBuilder. UriBuilderFactoryUriBuilder 一起提供了一种可插入机制,可以根据共享配置(例如基本 URL,编码首选项和其他详细信息) 从 URI 模板构建 URI.

您可以使用 UriBuilderFactory 配置 RestTemplateWebClient ,为自定义 URI 做准备. DefaultUriBuilderFactoryUriBuilderFactory 的默认实现,该实现在内部使用 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 Encoding

Spring MVC 和 Spring WebFlux

UriComponentsBuilder 在两个级别暴露编码选项:

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

考虑 ";",这在路径中是合法的但具有保留意义.第一个选项取代 ";" 在URI变量中使用 "%3B",但 URI 模板中没有.但在URI模板中没有.相比之下,第二个选项永远不会替换 ";",因为它是路经中的合法字符.

在大多数情况下, 第一个选项可能会产生预期结果, 因为它将 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")

WebClientRestTemplate 通过 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 模板进行编码,而是在将其扩展到模板之前通过 UriUtils#encodeUriVariables 对 URI 变量进行严格编码.

  • URI_COMPONENT: 在扩展 URI 变量后,使用 UriComponents#encode()(对应于先前列表中的第二个选项) 对 URI 组件值进行编码.

  • NONE: 未应用编码.

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

1.7. CORS

Spring MVC 允许您处理 CORS (跨源资源共享) . 本节介绍如何执行此操作.

1.7.1. 简介

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

Cross-Origin Resource Sharing (CORS) 是 大多数浏览器 实现的 W3C规范,它允许以灵活的方式指定哪些类型的跨域请求被授权, 而不是使用一些安全程度较低、功能较差的实现(如IFRAME或JSONP).

1.7.2. 处理

CORS 规范区分了预检查,简单和实际请求. 要了解 CORS 的工作原理,您可以 阅读本文以及其他许多内容,或者查看规范以获取更多详细信息.

Spring WebFlux HandlerMapping 为实现 CORS 提供内置支持. 成功将请求映射到处理程序后,HandlerMapping 实现检查给定请求和处理程序的 CORS 配置并采取进一步操作. 直接处理预检查请求,同时拦截,验证简单和实际的 CORS 请求,并设置所需的 CORS 响应头.

为了启用跨源请求(即,存在 Origin 头并且与请求的主机不同) ,您需要具有一些显式声明的 CORS 配置. 如果未找到匹配的 CORS 配置,则拒绝预检请求. 没有 CORS 头添加到简单和实际 CORS 请求的响应中,因此浏览器拒绝它们.

可以使用基于URL模式的 CorsConfiguration 映射单独 configured 每个 HandlerMapping. 在大多数情况下,应用程序使用 MVC Java 配置或 XML 命名空间来声明此类映射,这会导致将单个全局映射传递给所有 HandlerMapping 实例.

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

组合全局和本地配置的规则通常是附加的 - 例如,所有全局和所有本地源. 对于只能接受单个值的属性(例如 allowCredentialsmaxAge) , 本地会覆盖全局值. 有关详细信息,请参阅 CorsConfiguration#combine(CorsConfiguration).

要从source中了解更多信息或进行高级自定义,请查看后面的代码:

  • CorsConfiguration

  • CorsProcessor and DefaultCorsProcessor

  • AbstractHandlerMapping

1.7.3. @CrossOrigin

在带注解的控制器方法上使用 @CrossOrigin 注解启用跨源请求,如以下示例所示:

Java
@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 允许:

  • All origins.

  • All headers.

  • All HTTP methods to which the controller method is mapped.

默认情况下不启用 allowCredentials,因为它建立了一个信任级别,该信任级别暴露敏感的用户特定信息(例如 cookie 和 CSRF 令牌) ,并且只应在适当的地方使用.启用后, 必须将 allowOrigins 设置为一个或多个特定 domain (而不是特殊值 "*") , 或者可以使用 allowOriginPatterns 属性来动态匹配一组 origins.

maxAge 设置为 30 分钟.

@CrossOrigin 在类级别也受支持,并且由所有方法继承,如以下示例所示:

Java
@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 ,如以下示例所示:

Java
@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. 全局配置

除了细粒度,基于注解的配置以外,您可能还希望定义一些全局 CORS 配置. 您可以在任何 HandlerMapping 上单独设置基于URL的 CorsConfiguration 映射. 但是,大多数应用程序使用 WebFlux Java 配置来执行此操作.

默认情况下,全局配置启用以下内容:

  • All origins.

  • All headers.

  • GET, HEAD, and POST methods.

默认情况下不启用 allowCredentials,因为它建立了一个信任级别,该信任级别暴露敏感的用户特定信息(例如 cookie 和 CSRF 令牌) ,并且只应在适当的地方使用.启用后, 必须将 allowOrigins 设置为一个或多个特定 domain (而不是特殊值 "*") , 或者可以使用 allowOriginPatterns 属性来动态匹配一组 origins.

maxAge 设置为30分钟.

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

Java
@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

您可以通过内置的 CorsFilter 应用 CORS 支持.该功能非常适合functional endpoints.

如果您尝试将 CorsFilter 与 Spring Security 一起使用,请记住 Spring Security built-in support 了对 CORS 的支持.

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

Java
@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 Security

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

1.9. 视图技术

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

1.9.1. Thymeleaf

Thymeleaf 是一个现代服务器端 Java 模板引擎,它强调可以通过双击在浏览器中预览的自然 HTML 模板,这对于 UI 模板的独立工作(例如,由设计人员) 非常有用,而无需运行服务器. 如果您想要替换JSP, Thymeleaf 提供了一组最广泛的功能,使这种转换更容易. Thymeleaf 积极开发和维护. 有关更完整的介绍,请参阅Thymeleaf项目主页.

Thymeleaf 与 Spring WebFlux 的集成由 Thymeleaf 项目管理. 配置涉及一些 bean 声明, 例如 SpringResourceTemplateResolver, SpringWebFluxTemplateEngine, 和 ThymeleafReactiveViewResolver. 有关详细信息,请参阅 Thymeleaf+Springannouncement集成公告.

1.9.2. FreeMarker

Apache FreeMarker 是一个模板引擎,用于生成从 HTML 到电子邮件和其他的任何类型的文本输出. Spring Framework 有一个内置的集成,可以将 Spring WebFlux 与 FreeMarker 模板结合使用.

View 配置

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

Java
@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 模板.

FreeMarker 配置

通过设置 FreeMarkerConfigurer bean可以将 FreeMarker 的’Settings' 和 'SharedVariables' 值直接传递 Spring 管理的 FreeMarker 对象. freemarkerSettings 属性需要 java.util.Properties 对象. 而 freemarkerVariables 属性需要 java.util.Map . 以下示例显示了如何执行此操作:

Java
@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 文档

表单处理

Spring 本身提供了用于 JSP 的标签库,其中包含(当然还有很多) <spring:bind/> 标签,这个标签用来展示从 Web 上的 Validator 或业务层抛出的失败验证表单. Spring 还支持 FreeMarker 中的相同功能,并提供了方便的宏来生成表单输入元素.

绑定宏命令

spring-webflux.jar 包文件包含 FreeMarker 的一组标准宏,因此它们始终可用于经过适当配置的应用程序.

Spring 库中定义的某些宏被认为是内部的(私有的) ,但在宏定义中不存在这样的范围,其实所有宏都可以在调用代码和用户模板时看到. 以下各节仅集中于需要从模板中直接调用的宏, 如果希望直接查看宏代码, 那么可以看文件 spring.ftl,定义在 org.springframework.web.reactive.result.view.freemarker 包中.

有关绑定支持的更多详细信息,请参见简单绑定.

表单 Macros

有关Spring对FreeMarker模板的表单宏支持的详细信息,请参阅以下内容 Spring MVC文档的各个部分.

1.9.3. 脚本视图

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

Scripting Library Scripting Engine

Handlebars

Nashorn

Mustache

Nashorn

React

Nashorn

EJS

Nashorn

ERB

JRuby

String templates

Jython

Kotlin Script templating

Kotlin

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

您需要在类路径上安装脚本引擎,其详细信息因脚本引擎而异:

  • Nashorn Javascript 引擎提供了内置的 Java 8+. 强烈建议使用最新的可用更新版本.

  • 为了获得 JRuby 支持,应添加 JRuby 依赖性

  • 为了获得 Jython 支持,应添加 Jython 依赖性.

  • org.jetbrains.kotlin:kotlin-script-util 依赖和包含在 META-INF/services/javax.script.ScriptEngineFactory 文件里的 org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory 行应添加到 Kotlin 脚本支持中. 有关详细信息,请参阅此 示例 .

还需要为基于脚本的模板引擎添加依赖. 例如,对于 JavaScript,可以使用 WebJars.

脚本模板

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

Java
@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 提供对应用程序上下文,区域设置,模板加载器和 URL 的访问(自5.0起) .

Mustache.render() 方法会与本地兼容,因此可以直接调用.

如果模板化技术需要自定义,则可以提供实现自定义渲染函数的脚本. 例如, Handlerbars 需要在使用模板之前进行编译,并且需要使用 polyfill 以模拟服务器端脚本引擎中不可用的某些浏览器功能.

以下示例显示了如何执行此操作:

Java
@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
    }
}
NOTE: 当要求非线程安全地使用脚本引擎时,需要将 sharedEngine 的属性设置为 false ,因为模板库不是为了并发而设计的,具体可以看运行在 Nashorn 上的 Handlerbars 或 react. 据此,需要 Java 8u60+ 的版本来修复这个 this bug.
var window = {};

脚本 render.js 会在使用该模板之前被编译,一个好的产品应当保存和重用模板(使用缓存的方法) ,这样高效些. 这可以在脚本中完成,并且可以自定义它(例如管理模板引擎配置. 以下示例显示了如何执行此操作:

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

有关更多配置示例,请查看 Spring Framework 单元测试, Java, 和 resources.

1.9.4. JSON 和 XML

出于 内容协商,根据客户端请求的内容类型,能够在使用HTML模板呈现模型或以其他格式(例如 JSON 或 XML)呈现模型之间进行切换非常有用. 为了支持此操作,Spring WebFlux 提供了 HttpMessageWriterView,您可以使用它插入 spring-web 中的任何可用Codecs(编解码器)(例如 Jackson2JsonEncoder,Jackson2SmileEncoderJaxb2XmlEncoder).

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

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

1.10. HTTP 缓存

HTTP 缓存可以显着提高 Web 应用程序的性能. HTTP缓存围绕 Cache-Control 响应头,随后是条件请求头(例如 Last-ModifiedETag) . HTTP的响应头 Cache-Control 主要帮助私有缓存(比如浏览器端缓存) 和公共缓存(比如代理端缓存) 了解它们应该如果缓存HTTP响应. 如果内容未更改,则 ETag 头用于生成条件请求, 该条件请求可能导致 304 (NOT_MODIFIED) 没有正文. 可以认为它是 Last-Modified 头的一个更精细的后续版本.

本节介绍 Spring WebFlux 中可用的与HTTP缓存相关的选项.

1.10.1. CacheControl

CacheControl 支持配置与 Cache-Control 头相关的设置,并在许多地方被接受为参数:

虽然 RFC 7234 描述了 Cache-Control 响应头的所有可能的指令,但 CacheControl 类型采用面向用例的方法,该方法侧重于常见场景:

Java
// 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. Controllers

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

Java
@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 (NOT_MODIFIED) 响应. 否则,ETagCache-Control 头将添加到响应中.

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

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

    long eTag = ... (1)

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

    model.addAttribute(...); (3)
    return "myViewName";
}
1 特定于应用的计算.
2 响应已设置为 304(NOT_MODIFIED) - 无需进一步处理.
3 继续请求处理.
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(NOT_MODIFIED) - 无需进一步处理.
3 继续请求处理.

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

1.10.3. 静态资源

您应该使用 Cache-Control 和条件响应头来提供静态资源,以获得最佳性能. 请参阅有关静态资源的部分.

1.11. WebFlux 配置

WebFlux Java 配置提供了适用于大多数应用程序的默认配置以及配置 API 来对其进行自定义. 这意味您无需了解 Java 配置创建的基础 bean. 如果您想了解更多信息,请参阅 WebFluxConfigurationSupport 中查看它们,或者阅读有关特殊的 Bean 类型中内容的更多信息.

有关配置API中没有的高级自定义设置请参阅 高级配置模式.

1.11.1. 启用 WebFlux 配置

在 Java 配置中,您可以使用 @EnableWebFlux 注解启用 MVC 配置,如以下示例所示:

Java
@Configuration
@EnableWebFlux
public class WebConfig {
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig

前面的示例注册了许多 Spring MVC 基础结构的 beans,并适应类路径上可用的依赖(例如,JSON,XML 等的有效负载转换器) .

1.11.2. WebFlux config API

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

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    // Implement configuration methods...
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    // Implement configuration methods...
}

1.11.3. 转换,格式化

默认情况下,将安装各种数字和日期类型的格式化程序以及支持通过字段上的 @NumberFormat@DateTimeFormat 进行定制.

在 Java 配置中,您可以注册自定义格式化程序和转换器,如以下示例所示:

Java
@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 MVC 在解析和格式化日期值时会考虑请求区域设置. 这适用于使用 "input" 日期表示为字符串的表单. 但是,对于 "date" 和 "time" 表单字段,浏览器使用 HTML 规范中定义的固定格式. 在这种情况下,日期和时间格式可以按以下方式自定义:

Java
@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实现的更多信息,请参阅 FormatterRegistrar SPI和 FormattingConversionServiceFactoryBean.

1.11.4. 验证

默认情况下,如果类路径上存在Bean Validation (例如Hibernate Validator) ,则 LocalValidatorFactoryBean 将注册为全局 validator . 以便与 @ValidValidated 一起使用并在控制器方法参数上进行验证.

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

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

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

}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun getValidator(): Validator {
        // ...
    }

}

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

Java
@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 并使用 @Primary 标记它,以避免与 MVC 配置中声明的那个冲突.

1.11.5. 内容类型解析器

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

以下示例显示如何自定义请求的内容类型解析:

Java
@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 消息编解码器

以下示例显示如何自定义读取和写入请求和响应主体的方式:

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        // ...
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

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

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

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

同时,如果检测到在 classpath 路径下存在这些模块,也会自动地注册它们.

1.11.7. 视图解析

下面的示例显示如何配置视图解析:

Java
@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 Framework 集成的视图技术的快捷方式. 以下示例使用 FreeMarker(这也需要配置基础 FreeMarker 视图技术):

Java
@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 实现,如以下示例所示:

Java
@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
    }
}

为了支持 内容协商 并通过视图解析(除 HTML 之外)呈现其他格式,您可以基于 HttpMessageWriterView 实现配置一个或多个默认视图,该实现接受 spring-web 中任何可用的Codecs(编解码器). 以下示例显示了如何执行此操作:

Java
@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 集成的视图技术的更多信息,请参见 视图技术 .

1.11.8. 静态资源

此选项提供了一种从 Resource 库位置列表中使用静态资源的便捷方法

在下面的示例中,给定以 /resources 开头的请求,相对路径用于在 Web 应用程序根目录下或在或在 /static 下的类路径上查找静态资源. 资源的有效期为 1 年,以确保最大程度地使用浏览器缓存,并减少浏览器发出的 HTTP 请求. 如果返回 304 状态代码,Last-Modified 头也会计算到.

以下清单显示了如何使用 Java 配置执行此操作:

Java
@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 实现 和 ResourceTransformer 实现, 可用于创建用于使用优化资源的工具

VersionResourceResolver 可用于基于内容、固定应用程序版本或其他的 MD5 哈希计算的版本化资源url. ContentVersionStrategy(MD5 hash)方法是一个很好的选择, 有一些值得注意的例外,例如与模块加载器一起使用的 JavaScript 资源.

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

Java
@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 并应用完整的解析器和转换器链,例如插入版本. MVC 配置提供了 ResourceUrlProvider bean,因此可以将其注入到其他用户. 您还可以使用 ResourceUrlEncodingFilter 的 Thymeleaf、jsp、FreeMarker 和其他依赖于 HttpServletResponse#encodeURL 的 URL 标记来做重写转换.

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

请注意,当同时使用 EncodedResourceResolver(例如,用于提供 gzipped 或 brotli 编码的资源) 和 VersionedResourceResolver 时,必须按此顺序注册它们. 这可确保始终基于未编码的文件可靠地计算基于内容的版本.

WebJars 也支持使用 WebJarsResourceResolver 和自动注册,当 org.webjars:webjars-locator 存在于类路径中时. 解析器可以重写 URL 来包含 jar 的版本,也可以与传入的 URL 匹配,而不需要版本 . 例如, /jquery/jquery.min.js/jquery/1.2.0/jquery.min.js.

基于 ResourceHandlerRegistry 的 Java 配置提供了更多选项用于细粒度控制,例如,最后修改的行为和优化的资源解析。

1.11.9. 路径匹配

您可以自定义与路径匹配和 URL 处理相关的选项. 有关各个选项的详细信息,请参阅 PathMatchConfigurer javadoc.

以下示例显示如何在 Java 配置 PathMatchConfigurer:

Java
@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,并且去除了分号内容(即路径或矩阵变量). 这意味着,与 Spring MVC 不同,您无需指示是否解码请求路径,也无需指示是否出于路径匹配目的而删除分号内容.

Spring WebFlux 也不支持后缀模式匹配,这与 Spring MVC 不同,在 Spring MVC 中,我们也 建议不要依赖它.

1.11.10. WebSocketService

WebFlux Java 配置声明了一个 WebSocketHandlerAdapter bean, 它为调用 WebSocket 处理程序提供了支持. 这意味着要处理 WebSocket 握手请求, 剩下要做的就是通过 SimpleUrlHandlerMappingWebSocketHandler 映射到 URL.

在某些情况下, 可能需要使用提供的 WebSocketService 服务创建 WebSocketHandlerAdapter bean, 该服务允许配置 WebSocket 服务器属性. 例如:

Java
@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. 高级配置模式

@EnableWebFlux 导入 DelegatingWebFluxConfiguration 其中:

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

  • 检测到并委派到 WebFluxConfigurer 的自定义该配置

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

Java
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {

    // ...
}
Kotlin
@Configuration
class WebConfig : DelegatingWebFluxConfiguration {

    // ...
}

可以在 WebConfig 中保留现有的方法,但现在也可以重写基类中的 bean 声明,并且在类路径上仍然可以有任意数量的其他 WebMvcConfigurer .

1.12. HTTP/2

Servlet 4 容器需要支持 HTTP/2,Spring Framework 5 与 Servlet API 4 兼容. 从编程模型的角度来看,应用程序不需要特定的任何操作. 但是,存在与服务器配置相关的注意事项. 有关更多详细信息,请参阅 HTTP/2 wiki 页面.

2. WebClient

WebClient 是一个执行 HTTP 请求的非阻塞,响应式的客户端.客户端具有功能性,流式 API,具有用于声明式组合的响应式类型,请参见 Reactive 库. WebFlux 客户端和服务器依靠相同的非阻塞codecs 对请求和响应内容进行编码和解码.

WebClient 需要一个 HTTP 客户端库来执行请求。 有内置支持以下内容:

2.1. 配置

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

  • WebClient.create()

  • WebClient.create(String baseUrl)

上面的方法使用默认设置的 HttpClient 并且 io.projectreactor.netty:reactor-netty 位于 classpath 下

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

  • uriBuilderFactory: 自定义 UriBuilderFactory ,用做基本 url

  • defaultUriVariables: 扩展 URI 模板时使用的默认值。

  • defaultHeader: 每个请求的请求头.

  • defaultCookie: 每个请求的 cookie

  • defaultRequest: Consumer 自定义请求.

  • filter: 每个请求的过滤器.

  • exchangeStrategies: 自定义 HTTP 消息 reader/writer.

  • clientConnector: HTTP客户端库设置.

例如:

Java
WebClient client = WebClient.builder()
        .codecs(configurer -> ... )
        .build();
Kotlin
val webClient = WebClient.builder()
        .codecs { configurer -> ... }
        .build()

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

Java
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

Spring WebFlux 配置了在编解码器中缓冲内存中数据的 限制 ,以避免应用程序内存问题. 默认情况下,此配置为 256KB,如果这不足以满足您的用例,您将看到以下内容:

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer

要更改默认编解码器的限制,请使用以下命令::

Java
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. Reactor Netty

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

Java
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 中包含的全局 Reactor Netty 资源,包括事件循环线程和连接池. 这是推荐的模式,因为固定的共享资源是事件循环并发的首选. 在这种模式下,全局资源将保持活动状态,直到进程退出.

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

Java
@Bean
public ReactorResourceFactory reactorResourceFactory() {
    return new ReactorResourceFactory();
}
Kotlin
@Bean
fun reactorResourceFactory() = ReactorResourceFactory()

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

Java
@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.
Timeouts

要配置连接超时:

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

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

Java
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...

为所有请求配置响应超时:

Java
HttpClient httpClient = HttpClient.create()
        .responseTimeout(Duration.ofSeconds(2));

// Create WebClient...
Kotlin
val httpClient = HttpClient.create()
        .responseTimeout(Duration.ofSeconds(2));

// Create WebClient...

为特定请求配置响应超时:

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

只获取 body

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. Exchange

exchangeToMono()exchangeToFlux() 方法(或 Kotlin 中的 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()
        }
  }

使用上述时,返回的 MonoFlux 完成后,响应正文被检查,如果没有被消费,它将被释放以防止内存和连接泄漏。 因此,响应无法在下游进一步解码。 这取决于提供的函数来声明如何在需要时解码响应。

2.4. Request Body

可以从 ReactiveAdapterRegistry 处理的任何异步类型对请求主体进行编码,如 Mono 或 Kotlin Coroutines 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. Form Data

要发送表单数据,可以提供 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. Multipart Data

要发送多部分数据,您需要提供一个 MultiValueMap<String, ?> 其值可以是代表部件内容的 Object 实例或代表部件内容和 header 的 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. Client Filters

您可以通过 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()) {
                return@flatMap response.releaseBody()
                        .then(renewToken())
                        .flatMap { token: String? ->
                            val newRequest = ClientRequest.from(request).build()
                            next.exchange(newRequest)
                        }
            } else {
                return@flatMap Mono.just(response)
            }
        }
    }
}

2.6. Attributes

您可以向请求添加属性。 如果您想传递信息,可以通过过滤器链并影响给定请求的过滤器行为:

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. Context

Attributes 提供了一种简便的方式来将信息传递给过滤器链,但它们只影响当前请求。如果你想传递信息 传播到其他嵌套的请求,例如通过 flatMap,或在之后执行, 例如,通过 concatMap,那么你将需要使用 Reactor Context

Reactor 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. Synchronous Use

通过在结果末尾进行阻塞,可以以同步方式使用 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())
    }

以上仅是一个示例.还有许多其他模式和运算符可用于构建响应式管道,该响应式管道可进行许多远程调用(可能是嵌套的,相互依赖的),而不会阻塞到最后.

使用 FluxMono,您永远不必阻塞 Spring MVC 或 Spring WebFlux 控制器. 只需从 controller 方法返回结果类型即可. 相同的原则适用于 Kotlin Coroutines 和 Spring WebFlux,只需在控制器方法中使用暂停功能或返回 Flow 即可.

2.9. Testing

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

3. WebSockets

这一部分介绍 Spring 框架在 reactive-stack 应用程序中对 WebSocket 消息传递的支持,

3.1. WebSocket简介

WebSocket 协议 RFC 6455,提供了一种标准化方法,可通过单个 TCP 连接在客户端和服务器之间建立全双工双向通信通道. 它是来自 HTTP 的不同 TCP 协议,但设计为通过 HTTP 工作并使用 80 和 443 端口,并允许重用现有防火墙规则.

WebSocket 交互以 HTTP 请求开始,该 HTTP 请求使用HTTP Upgrade 头进行升级,或者在这种情况下,切换到 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 如何工作的完整介绍超出了本文档的范围.请参阅 RFC 6455,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 客户端和服务器可以通过 HTTP 握手请求上的 Sec-WebSocket-Protocol 头协商使用更高级别的消息传递协议(例如,STOMP) . 如果没有,他们需要提出自己的惯例.

3.1.2. 何时使用WebSockets

WebSockets 可以使网页变得动态和交互. 但是,在许多情况下,Ajax 和 HTTP 流式传输或长轮询的组合可以提供简单有效的解决方案.

例如,新闻,邮件和社交订阅资源需要动态更新,但是正常更新的间隔时间为几分钟. 另一方面,协作,游戏和财务应用程序需要更接近实时.

仅延迟并不是使用 WebSocket 的决定因素. 如果消息量相对较低(例如,监视网络故障) ,HTTP 流式传输或轮询可以提供有效的解决方案. 而当需要它是低延迟,高频率和高容量的组合,那使用 WebSocket 是最佳选择.

还要记住,通过 Internet,受控制之外的限制性代理可能会阻止 WebSocket 交互,因为它们未配置为传递 Upgrade 头, 或者因为它们关闭看似空闲的长期连接. 这意味着将 WebSocket 用于防火墙内的内部应用程序是一个比面向公众的应用程序更直接的决策.

3.2. WebSocket API

Spring Framework 提供了一个 WebSocket API,您可以使用它来编写处理 WebSocket 消息的客户端和服务器端应用程序.

3.2.1. Server

创建 WebSocket 服务器,首先创建一个 WebSocketHandler 如下

Java
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 ,如以下示例所示:

Java
@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 Config 则不需要进行下一步操作, 否则, 如果不使用 WebFlux config 则需要声明一个 WebSocketHandlerAdapter 如下:

Java
@Configuration
class WebConfig {

    // ...

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}
Kotlin
@Configuration
class WebConfig {

    // ...

    @Bean
    fun handlerAdapter() =  WebSocketHandlerAdapter()
}

3.2.2. WebSocketHandler

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

WebSocketSession 方法 描述

Flux<WebSocketMessage> receive()

提供对入站消息流的访问,并在关闭连接时完成.

Mono<Void> send(Publisher<WebSocketMessage>)

获取传出消息的源,编写消息,然后返回 Mono<Void>,该源在完成源并完成写入后即完成.

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

  • 入站或出站消息流都已完成.

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

  • 在选定的时间点,通过 WebSocketSessionclose 方法.

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

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

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

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

Java
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> .

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

Java
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 部分在有关数据缓冲区和编解码器的部分中有更多内容. 要理解的关键点是,在诸如 Netty 之类的某些服务器上,字节缓冲区是池化的,并且对引用计数进行计数,并且在消耗字节缓冲时必须将其释放以避免内存泄漏.

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

3.2.4. 握手

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

HandshakeWebSocketService 暴露了一个 sessionAttributePredicate 属性,该属性允许设置 Predicate<String>WebSession 中提取属性并将其插入到 WebSocketSession 的属性中.

3.2.5. Server 配置

每个服务器的 RequestUpgradeStrategy 暴露了可用于基础 WebSocket 引擎的 WebSocket 相关配置选项. 使用 WebFlux Java 配置时, 您可以自定义这样的属性, 如 WebFlux Config 的相应部分所示, 否则, 如果不使用 WebFlux 配置, 请使用下面的配置:

Java
@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. CORS

配置 CORS 并限制对 WebSocket 端点的访问的最简单方法是让 WebSocketHandler 实现 CorsConfigurationSource 并返回带有允许的源, header 和其他详细信息的 CorsConfiguration. 如果无法执行此操作,则还可以在 SimpleUrlHandler 上设置 corsConfigurations 属性,以通过 URL 模式指定 CORS 设置. 如果同时指定了两者,则使用 CorsConfiguration 上的 combine 方法将它们合并.

3.2.7. Client

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

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

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

Java
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. 测试

spring-test 模块提供了 ServerHttpRequest,ServerHttpResponseServerWebExchange 的模拟实现. 有关模拟对象的讨论,请参见 Spring Web Reactive

WebTestClient 建立在这些模拟请求和响应对象的基础上,以提供对无需 HTTP 服务器即可测试 WebFlux 应用程序的支持.您也可以将 WebTestClient 用于端到端集成测试.

5. RSocket

本节描述了 Spring Framework 对 RSocket 协议的支持.

5.1. 概述

RSocket 是通过 TCP,WebSocket 和其他字节流传输进行多路复用,双工通信的应用协议,RSocket 使用以下交互模型:

  • Request-Response — 这是最典型也最常见的模式.发送方在发送消息给接收方之后,等待与之对应的响应消息.

  • Request-Stream — 发送方的每个请求消息,都对应于接收方的一个消息流作为响应.

  • Channel — 在发送方和接收方之间建立一个双向传输的通道.

  • Fire-and-Forget — 发送方的请求消息没有与之对应的响应.

一旦建立了初始连接,由于双方变得对称,并且双方都可以发起上述交互之一,因此"客户端”与"服务器”的区别将消失. 这就是为什么在协议中将参与方称为"请求者”和"响应者”,而将上述交互称为"请求流”或简称为"请求”的原因.

这些是 RSocket 协议的主要功能和优势:

  • Reactive Streams 的语义跨过网络边界 — 对于流请求,例如 Request-StreamChannel,在请求者和应答者之间通过背压信号进行交流。允许请求者在源处降低应答者的速度,从而减少对网络层拥塞控制的依赖。以及在网络级别或任何级别对缓冲的需求.

  • Request throttling — 此特性在 LEASE 后被命名为 "Leasing", 可以从两端发送,以限制在给定时间内允许的另一端请求的总数。租约定期更新.

  • Session resumption — 这是为失去连接而设计的,需要一些状态来维护。状态管理对应用程序来说是透明的,并且与背压结合使用效果很好,背压可以在可能的情况下停止生产,并减少所需的状态量.

  • 分解和重新组装大消息。

  • Keepalive (心跳).

RSocket 有多种语言的 实现.https://github.com/rsocket/rsocket-java[Java library] 构建在 Project Reactor 和用于传输的 Reactor Netty 之上.这意味着来自应用程序中的响应流发布者的信号通过 RSocket 在网络中透明地传播.

5.1.1. 协议

RSocket 的优点之一是,它在线路上具有定义明确的行为,并且易于阅读的 规范 以及某些协议 扩展.

Connecting

最初,客户端通过一些低级流传输(例如 TCP 或 WebSocket)连接到服务器,然后将 SETUP 帧发送到服务器以设置连接参数.

服务器可以拒绝 SETUP 帧,但是通常在发送(对于客户端)和接收(对于服务器)之后,双方都可以开始发出请求,除非 SETUP 指示使用租赁语义来限制请求的数量. 在这种情况下,双方都必须等待另一端的 LEASE 帧以允许发出请求.

Making Requests

建立连接后,双方都可以通过帧 REQUEST_RESPONSE,REQUEST_STREAM,REQUEST_CHANNELREQUEST_FNF 中的一个来发起请求. 这些帧中的每一个都将一条消息从请求者传送到响应者.

然后,响应者可以返回带有响应消息的 PAYLOAD 帧,在 REQUEST_CHANNEL 的情况下,请求者还可以发送带有更多请求消息的 PAYLOAD 帧.

当请求涉及消息流(例如 Request-Stream 和 Channel)时,响应者必须遵守来自请求者的需求信号. 需求表示为许多消息. 初始需求在 REQUEST_STREAMREQUEST_CHANNEL 帧中指定. 后续需求通过 REQUEST_N 帧发出信号.

每一端还可以通过 METADATA_PUSH 帧发送元数据通知,该元数据通知与任何单独的请求无关,而与整个连接有关.

Message Format

RSocket 消息包含数据和元数据. 元数据可用于发送路由,安全令牌等.数据和元数据的格式可以不同. 每个 MIME 类型都在 SETUP 帧中声明,并应用于给定连接上的所有请求.

尽管所有消息都可以具有元数据,但通常每个请求都包含诸如路由之类的元数据,因此仅包含在请求的第一条消息中,即与帧 REQUEST_RESPONSE,REQUEST_STREAM,REQUEST_CHANNELREQUEST_FNF 中的一个一起使用.

协议扩展定义了用于应用程序的通用元数据格式:

5.1.2. Java 实现

RSocket 的 Java implementation 基于 Project Reactor 构建. TCP 和 WebSocket 的传输建立在 Reactor Netty 上. 作为 Reactive Streams 库,Reactor 简化了实现协议的工作. 对于应用程序,自然而然的选择是将 FluxMono 与声明性运算符和透明背压支持一起使用.

RSocket Java 中的 API 故意是最小且基本的. 它侧重于协议功能,而将应用程序编程模型(例如 RPC 代码生成与其他)作为一个更高级别的独立关注点.

主 contract Java implementation 对四种请求交互类型进行建模,其中 Mono 表示单个消息的承诺,Flux 表示消息流,而 io.rsocket.Payload 通过访问实际数据消息来访问数据和元数据作为字节缓冲区. RSocket 合同是对称使用的. 对于请求,将为应用程序提供一个 RSocket 来执行请求. 为了响应,该应用程序实现 RSocket 来处理请求.

这并不意味着要进行全面介绍. 在大多数情况下,Spring 应用程序将不必直接使用其 API. 但是,独立于 Spring 来看或尝试 RSocket 可能很重要. RSocket Java 存储库包含许多 示例应用程序 ,以演示其API和协议功能.

5.1.3. Spring 支持

spring-messaging 模块包含以下内容

  • RSocketRequester — 流式的 API 通过 io.rsocket.RSocket 进行请求,并进行数据和元数据编码/解码.

  • Annotated Responders — @MessageMapping 响应注解处理方法

spring-web 模块包含 RSocket 应用程序可能需要的 EncoderDecoder 实现,例如 Jackson CBOR/JSON 和 Protobuf. 它还包含 PathPatternParser,可以将其插入以进行有效的路由匹配.

Spring Boot 2.2 支持通过 TCP 或 WebSocket 站立 RSocket 服务器,包括在 WebFlux 服务器中通过 WebSocket 暴露 RSocket 的选项. RSocketRequester.BuilderRSocketStrategies 还具有客户端支持和自动配置. 有关更多详细信息,请参见 Spring Boot 参考中的 RSocket 部分.

Spring Security 5.2 提供了 RSocket 支持.

Spring Integration 5.2 提供了入站和出站网关以与 RSocket 客户端和服务器进行交互. 有关更多详细信息,请参见 Spring Integration 参考手册.

Spring Cloud Gateway 支持 RSocket 连接.

5.2. RSocketRequester

RSocketRequester 提供了一个流式的 API 来执行 RSocket 请求,接受和返回数据和元数据的对象,而不是底层数据缓冲区. 它可以对称地用于从客户端发出请求和从服务器发出请求.

5.2.1. Client Requester

要在客户端获得 RSocketRequester,需要连接到服务器,该服务器涉及带有发送连接设置的 RSocket SETUP 帧. RSocketRequester 为此提供了一个生成器. 可以帮助准备 io.rsocket.core.RSocketConnector 构建,包括 SETUP 帧的连接设置.

这是使用默认设置进行连接的最基本方法:

Java
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)

上面没有立即连接. 发出请求时, 将透明地建立并使用共享连接.

Connection Setup

RSocketRequester.Builder 提供了以下自定义初始 SETUP 帧的功能:

  • dataMimeType(MimeType) — 设置连接数据的 mime 类型.

  • metadataMimeType(MimeType) — 设置连接上元数据的 mime 类型.

  • setupData(Object) — 包含在 SETUP 中的数据.

  • setupRoute(String, Object…​) — 在元数据中路由以包含在 SETUP 中.

  • setupMetadata(Object, MimeType) — 其他要包含在 SETUP 中的元数据.

对于数据,默认的 mime 类型是从第一个配置的 Decoder 继承的. 对于元数据,默认的 mime 类型是 复合元数据,它允许每个请求有多个元数据值和 mime 类型对. 通常,两者都不需要更改.

SETUP 帧中的数据和元数据是可选的. 在服务器端,@ConnectMapping 方法可用于处理连接的开始以及 SETUP 帧的内容. 元数据可用于连接级别的安全性.

Strategies

RSocketRequester.Builder 接受 RSocketStrategies 来配置请求者. 您需要使用它来提供编码器和解码器,以对数据和元数据值进行(反)序列化. 默认情况下,仅注册 spring-core 中用于 String, byte[]ByteBuffer 的基本编解码器. 添加 spring-web 可以访问更多可以注册的内容,如下所示:

Java
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 配置中声明它.

Client Responders

RSocketRequester.Builder 可用于配置响应器以响应来自服务器的请求.

您可以根据服务器上使用的相同基础结构,使用带注解的处理程序来进行客户端响应,但是通过编程方式进行了以下注册:

Java
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,以提高效率路线匹配.
2 创建包含 @MessageMapping@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,以提高效率路线匹配.
2 创建包含 @MessageMapping@ConnectMapping 方法的响应者.
3 注册响应者.

请注意,以上只是设计用于客户端响应程序的程序化注册的快捷方式. 对于客户端响应者处于 Spring 配置的替代方案,您仍然可以将 RSocketMessageHandler 声明为 Spring Bean,然后按如下所示进行应用:

Java
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)

对于上述情况,您可能还需要使用 RSocketMessageHandler 中的 setHandlerPredicate 来切换到用于检测客户端响应程序的其他策略,例如 基于自定义注解,例如 @RSocketClientResponder 与默认 @Controller. 在客户端和服务器或同一应用程序中有多个客户端的情况下,这是必需的.

有关编程模型的更多信息,请参见 Annotated Responders.

Advanced

RSocketRequesterBuilder 提供了一个回调,用于从 RSocket Java 中暴露底层 io.rsocket.core.RSocketConnector,以获取更多有关 keepalive 间隔,会话恢复,拦截器等的配置选项. 您可以按以下方式在该级别上配置选项:

Java
RSocketRequester requester = RSocketRequester.builder()
    .rsocketConnector(connector -> {
        // ...
    })
    .tcp("localhost", 7000);
Kotlin
val requester = RSocketRequester.builder()
        .rsocketConnector {
            //...
        }
        .tcp("localhost", 7000)

5.2.2. Server Requester

从服务器向连接的客户端发出请求是从服务器获取连接客户端的请求者的问题.

Annotated Responders 中,@ConnectMapping@MessageMapping 方法支持 RSocketRequester 参数. 使用它来访问连接的请求者. 请记住,@ConnectMapping 方法本质上是 SETUP 帧的处理程序,必须在请求开始之前对其进行处理. 因此,必须从一开始就将请求与处理分离. 例如:

Java
@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. Requests

有了 clientserver 请求者后,可以按以下方式发出请求:

Java
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) 方法还接受任何 Reactive Streams Publisher,包括 FluxMono,以及在 ReactiveAdapterRegistry 中注册的任何其他值的生产者. 对于产生相同类型值的多值发布器(例如 Flux),请考虑使用重载 data 方法之一, 以避免对每个元素进行类型检查和 Encoder 查找:

data(Object producer, Class<?> elementClass);
data(Object producer, ParameterizedTypeReference<?> elementTypeRef);

data(Object) 步骤是可选的. 跳过不发送数据的请求:

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

如果使用 复合元数据 (默认设置)并且注册的 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,请使用返回 Mono<Void>send() 方法. 请注意,Mono 仅指示消息已成功发送,而不是已被处理.

Metadata-Push 使用 sendMetadata() 方法并返回一个 Mono<Void>.

5.3. Annotated Responders

RSocket 响应器可以实现为 @MessageMapping@ConnectMapping 方法. @MessageMapping 方法处理单个请求,而 @ConnectMapping 方法处理连接级事件(设置和元数据推送). 对称支持带注解的响应者,用于从服务器端响应和从客户端端响应.

5.3.1. Server Responders

要在服务器端使用带注解的响应者,请将 RSocketMessageHandler 添加到您的 Spring 配置中,以使用 @MessageMapping@ConnectMapping 方法检测 @Controller 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 默认情况下支持 复合路由 元数据. 如果需要切换到其他 mime 类型或注册其他元数据 mime 类型,则可以设置其 MetadataExtractor.

您需要设置支持元数据和数据格式所需的 EncoderDecoder 实例. 您可能需要 spring-web 模块来实现编解码器.

默认情况下,SimpleRouteMatcher 用于通过 AntPathMatcher 匹配路由. 我们建议从 spring-web 插入 PathPatternRouteMatcher 以进行有效的路由匹配. RSocket 路由可以是分层的,但不是 URL 路径. 两个路由匹配器都配置为使用 "." 默认为分隔符,并且没有 HTTP 网址那样的 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. Client Responders

需要在 RSocketRequester.Builder 中配置客户端的带注解的响应者. 有关详细信息,请参阅Client Responders.

5.3.3. @MessageMapping

serverclient 响应程序配置到位后,可以按以下方式使用 @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" 的请求-流交互. 它支持灵活的方法签名,并可以选择使用以下方法参数:

方法参数 描述

@Payload

请求的有效负载. 这可以是诸如 MonoFlux 之类的异步类型的具体值. Note: 注解的使用是可选的. 并非简单类型并且不是其他任何受支持参数的方法参数都假定为预期的有效负载.

RSocketRequester

向远端发出请求的请求者.

@DestinationVariable

根据映射模式中的变量从路线提取的值,例如 @MessageMapping("find.radar.{id}").

@Header

MetadataExtractor 中所述注册要提取的元数据值.

@Headers Map<String, Object>

MetadataExtractor 中所述,注册所有用于提取的元数据值.

返回值应为一个或多个要序列化为响应有效负载的对象. 可以是诸如 MonoFlux 的异步类型,具体值,也可以是 void 或无值的异步类型,例如 Mono<Void>.

@MessageMapping 方法支持的 RSocket 交互类型由输入(即 @Payload 参数)和输出的基数确定,其中基数表示以下内容:

基数 描述

1

显式值或单值异步类型(如 Mono<T>) .

Many

多值异步类型,例如 Flux<T>.

0

对于输入,这意味着该方法没有 @Payload 参数.

对于输出,这是空的或无值异步类型,例如 Mono <Void>.

下表显示了所有输入和输出基数组合以及相应的交互类型:

Input Cardinality Output Cardinality Interaction Types

0, 1

0

Fire-and-Forget, Request-Response

0, 1

1

Request-Response

0, 1

Many

Request-Stream

Many

0, 1, Many

Request-Channel

5.3.4. @ConnectMapping

@ConnectMapping 在 RSocket 连接开始时处理 SETUP 帧,并通过 METADATA_PUSH 帧(即 io.rsocket.RSocket 中的 metadataPush(Payload) 处理任何后续的元数据推送通知.

@ConnectMapping 方法支持与 @MessageMapping 相同的参数,但基于 SETUPMETADATA_PUSH 帧中的元数据和数据. @ConnectMapping 可以具有将处理范围缩小到元数据中具有路由的特定连接的模式,或者,如果未声明任何模式,则所有连接都匹配.

@ConnectMapping 方法无法返回数据,必须使用 voidMono<Void> 作为返回值进行声明. 如果处理为新连接返回错误,则连接被拒绝. 不得阻止向 RSocketRequester 发出连接请求的处理. 有关详细信息,请参见Server Requester.

5.4. MetadataExtractor

响应者必须解释元数据. 复合元数据 允许独立格式化的元数据值(例如,用于路由,安全性和跟踪),每个元数据值都有自己的mime类型. 应用程序需要一种配置要支持的元数据 mime 类型的方法,以及一种访问提取值的方法.

MetadataExtractor 是一种协议,用于获取序列化的元数据并返回解码的名称/值对,然后可以按名称对标题进行访问,例如通过带注解的处理程序方法中的 @Header 进行访问.

可以为 DefaultMetadataExtractor 提供 Decoder 实例以解码元数据. 开箱即用,它具有对 "message/x.rsocket.routing.v0" 的内置支持,它可以解码为 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)
}

通过 RSocketStrategies 配置 MetadataExtractor 时,可以让 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. Reactive 库

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

对于带注解的控制器,WebFlux 透明的自动选择合适的 reactive 库.这是在 ReactiveAdapterRegistry 的帮助下完成的,该工具为 reactive 库和其他异步类型提供了可插入的支持. 该注册表具有对 RxJava 3, Kotlin coroutines 和 SmallRye Mutiny 的内置支持,但您也可以注册其他注册表.

从 Spring Framework 5.3.11 开始,根据 RxJava 自己的 EOL 建议和对 RxJava 3 的升级建议,不推荐对 RxJava 1 和 2 的支持。

对于功能性API(例如 功能端点,WebClient 等),WebFlux API 的一般规则适用于- FluxMono 作为返回值,而 Reactive Streams Publisher 作为输入. 提供发布者(无论是自定义的还是来自其他响应式库的发布者)时,只能将其视为语义未知(0..N)的流.但是,如果知道语义,则可以使用 FluxMono.from(Publisher) 对其进行包装,而不用传递原始 Publisher.

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