这部分文档涵盖了对基于Reactive Streams API 构建的响应式堆栈 Web 应用程序的支持,以在 Netty、Undertow 和 Servlet 3.1+ 容器等非阻塞服务器上运行。各个章节涵盖了Spring WebFlux框架、响应式、测试WebClient支持和响应式库。对于 Servlet-stack Web 应用程序,请参阅Web on Servlet Stack

1. Spring WebFlux

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

两个 Web 框架都反映了它们的源模块(spring-webmvcspring-webflux)的名称,并在 Spring 框架中并存。每个模块都是可选的。应用程序可以使用一个或另一个模块,或者在某些情况下,两者都使用——例如,Spring MVC 控制器和响应式WebClient.

1.1。概述

为什么要创建 Spring WebFlux?

部分答案是需要一个非阻塞 Web 堆栈来处理具有少量线程的并发性并以更少的硬件资源进行扩展。Servlet 3.1 确实为非阻塞 I/O 提供了 API。但是,使用它会远离 Servlet API 的其余部分,其中合约是同步的(Filter, Servlet)或阻塞的(getParameter, getPart)。这是一个新的通用 API 作为跨任何非阻塞运行时的基础的动机。这很重要,因为服务器(例如 Netty)在异步、非阻塞空间中已经很成熟。

答案的另一部分是函数式编程。就像在 Java 5 中添加注释创造了机会(例如带注释的 REST 控制器或单元测试)一样,在 Java 8 中添加 lambda 表达式为 Java 中的函数式 API 创造了机会。这对于允许异步逻辑的声明式组​​合的非阻塞应用程序和延续式 API(由 ReactiveX 推广)是一个CompletableFuture福音在编程模型级别,Java 8 使 Spring WebFlux 能够提供功能性 Web 端点以及带注释的控制器。

1.1.1。定义“反应性”

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

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

我们在 Spring 团队中还有另一个与“反应式”相关的重要机制,那就是非阻塞背压。在同步的命令式代码中,阻塞调用是一种自然的背压形式,迫使调用者等待。在非阻塞代码中,控制事件的速率变得很重要,这样快速的生产者就不会压倒它的目的地。

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

常见问题:如果出版商不能放慢速度怎么办?
Reactive Streams 的目的只是建立机制和边界。如果发布者不能放慢速度,它必须决定是缓冲、丢弃还是失败。

1.1.2。反应式 API

Reactive Streams 在互操作性方面发挥着重要作用。它对库和基础设施组件很感兴趣,但作为应用程序 API 用处不大,因为它太低级了。应用程序需要更高级别、更丰富的功能 API 来组合异步逻辑——类似于 Java 8 StreamAPI,但不仅适用于集合。这就是响应式库所扮演的角色。

Reactor是 Spring WebFlux 的首选响应式库。它 通过与 ReactiveX运算符词汇表对齐的一组丰富的运算符提供了0..1( ) 和 0..N( ) 的数据序列MonoFluxAPI 类型。Reactor 是一个 Reactive Streams 库,因此,它的所有操作符都支持非阻塞背压。Reactor 非常关注服务器端 Java。它是与 Spring 密切合作开发的。MonoFlux

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

除了 Reactive API,WebFlux 还可以与 Kotlin 中的Coroutines API 一起使用,这提供了一种更加命令式的编程风格。以下 Kotlin 代码示例将随 Coroutines API 一起提供。

1.1.3。编程模型

spring-web模块包含作为 Spring WebFlux 基础的响应式基础,包括 HTTP 抽象、支持服务器的响应式流适配器、编解码器以及与 Servlet API 相当但具有非阻塞契约的核心WebHandlerAPI 。

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

  • 带注释的控制器:与 Spring MVC 一致,并基于spring-web模块中的相同注释。Spring MVC 和 WebFlux 控制器都支持响应式(Reactor 和 RxJava)返回类型,因此很难区分它们。一个显着的区别是 WebFlux 还支持响应式@RequestBody参数。

  • 函数式端点:基于 Lambda 的轻量级函数式编程模型。您可以将其视为应用程序可用于路由和处理请求的小型库或一组实用程序。带注释的控制器的最大区别在于,应用程序负责从头到尾处理请求,而不是通过注释声明意图并被回调。

1.1.4。适用性

Spring MVC 还是 WebFlux?

一个自然的问题要问,但它会建立一个不合理的二分法。实际上,两者一起工作以扩大可用选项的范围。两者旨在实现彼此的连续性和一致性,它们可以并排使用,并且来自每一方的反馈对双方都有好处。下图显示了两者的关系、它们的共同点以及各自独特的支持:

spring mvc 和 webflux venn

我们建议您考虑以下具体点:

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

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

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

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

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

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

  • 如果您有一个大型团队,请记住向非阻塞、函数式和声明式编程转变的陡峭学习曲线。在没有完全开关的情况下启动的一种实用方法是使用 reactive WebClient。除此之外,从小处着手并衡量收益。我们预计,对于广泛的应用,这种转变是不必要的。如果您不确定要寻找什么好处,请先了解非阻塞 I/O 的工作原理(例如,单线程 Node.js 上的并发性)及其影响。

1.1.5。服务器

Spring WebFlux 在 Tomcat、Jetty、Servlet 3.1+ 容器以及非 Servlet 运行时(如 Netty 和 Undertow)上受支持。所有服务器都适用于低级 通用 API ,因此可以跨服务器支持更高级别的 编程模型。

Spring WebFlux 没有内置支持来启动或停止服务器。但是,很容易从 Spring 配置和WebFlux 基础设施组装应用程序并 使用几行代码运行它。

Spring Boot 有一个 WebFlux 启动器,可以自动执行这些步骤。默认情况下,启动器使用 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(以及一般的非阻塞服务器)中,假设应用程序不会阻塞。因此,非阻塞服务器使用一个小的、固定大小的线程池(事件循环工作者)来处理请求。

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

如果您确实需要使用阻塞库怎么办?Reactor 和 RxJava 都提供了 publishOn在不同线程上继续处理的操作符。这意味着有一个简单的逃生舱口。但是请记住,阻塞 API 并不适合这种并发模型。

可变状态

在 Reactor 和 RxJava 中,您通过运算符声明逻辑。在运行时,会形成一个反应式管道,其中数据在不同的阶段按顺序处理。这样做的一个关键好处是它使应用程序不必保护可变状态,因为该管道中的应用程序代码永远不会同时调用。

线程模型

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

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

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

  • Reactor 和 RxJava 提供线程池抽象,称为调度程序,与 publishOn用于将处理切换到不同线程池的运算符一起使用。调度程序的名称暗示了特定的并发策略——例如,“并行”(用于 CPU 密集型工作,线程数量有限)或“弹性”(I/O 密集型工作,有大量线程)。如果您看到这样的线程,则意味着某些代码正在使用特定的线程池Scheduler策略。

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

配置

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

1.2. 反应核心

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

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

    • HttpHandler:具有非阻塞 I/O 和 Reactive Streams 背压的 HTTP 请求处理的基本合同,以及用于 Reactor Netty、Undertow、Tomcat、Jetty 和任何 Servlet 3.1+ 容器的适配器。

    • WebHandlerAPI:用于请求处理的稍微更高级别的通用 Web API,在其之上构建了具体的编程模型,例如带注释的控制器和功能端点。

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

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

1.2.1。HttpHandler

HttpHandler 是一个简单的契约,只有一个方法来处理请求和响应。它是故意最小化的,它的主要和唯一目的是对不同的 HTTP 服务器 API 进行最小的抽象。

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

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

网状

网络接口

反应堆网

暗流

Undertow API

spring-web:Undertow 到 Reactive Streams 的桥梁

雄猫

Servlet 3.1 非阻塞 I/O;Tomcat API 读写 ByteBuffers vs byte[]

spring-web:Servlet 3.1 到 Reactive Streams 的非阻塞 I/O 桥接器

码头

Servlet 3.1 非阻塞 I/O;Jetty API 编写 ByteBuffers vs byte[]

spring-web:Servlet 3.1 到 Reactive Streams 的非阻塞 I/O 桥接器

Servlet 3.1 容器

Servlet 3.1 非阻塞 I/O

spring-web:Servlet 3.1 到 Reactive Streams 的非阻塞 I/O 桥接器

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

服务器名称 组号 工件名称

反应堆网

io.projectreactor.netty

反应堆网络

暗流

io.undertow

下核

雄猫

org.apache.tomcat.embed

tomcat-embed-core

码头

org.eclipse.jetty

码头服务器,码头servlet

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

反应堆网

java
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();
科特林
val handler: HttpHandler = ...
val adapter = ReactorHttpHandlerAdapter(handler)
HttpServer.create().host(host).port(port).handle(adapter).bind().block()

暗流

java
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
科特林
val handler: HttpHandler = ...
val adapter = UndertowHttpHandlerAdapter(handler)
val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build()
server.start()

雄猫

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();
科特林
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()

码头

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();
科特林
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+ 容器

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

1.2.2。WebHandlerAPI

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

虽然HttpHandler有一个简单的目标来抽象不同 HTTP 服务器的使用,但 WebHandlerAPI 旨在提供 Web 应用程序中常用的更广泛的功能集,例如:

  • 具有属性的用户会话。

  • 请求属性。

  • 已解决LocalePrincipal为请求。

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

  • 多部分数据的抽象。

  • 和更多..

特殊豆类

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

豆名 豆类 数数 描述

<任何>

WebExceptionHandler

0..N

WebFilter为来自实例 链和目标的异常提供处理WebHandler。有关更多详细信息,请参阅异常

<任何>

WebFilter

0..N

在过滤器链的其余部分和目标之前和之后应用拦截样式逻辑WebHandler。有关更多详细信息,请参阅过滤器

webHandler

WebHandler

1

请求的处理程序。

webSessionManager

WebSessionManager

0..1

WebSession通过 上的方法公开的实例的管理器ServerWebExchangeDefaultWebSessionManager默认。

serverCodecConfigurer

ServerCodecConfigurer

0..1

访问HttpMessageReader实例以解析表单数据和多部分数据,然后通过ServerWebExchange. ServerCodecConfigurer.create()默认。

localeContextResolver

LocaleContextResolver

0..1

LocaleContext通过 on 方法公开的解析器ServerWebExchangeAcceptHeaderLocaleContextResolver默认。

forwardedHeaderTransformer

ForwardedHeaderTransformer

0..1

用于处理转发的类型标头,通过提取和删除它们或仅删除它们。默认不使用。

表格数据

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

java
Mono<MultiValueMap<String, String>> getFormData();
科特林
suspend fun getFormData(): MultiValueMap<String, String>

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

多部分数据

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

java
Mono<MultiValueMap<String, Part>> getMultipartData();
科特林
suspend fun getMultipartData(): MultiValueMap<String, Part>

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

要以流方式解析多部分数据,您可以改用Flux<Part>从 an 返回的 HttpMessageReader<Part>。例如,在带注释的控制器中,使用 类似@RequestPart暗示Map的方式通过名称访问各个部分,因此需要完整解析多部分数据。相比之下,您可以使用@RequestBody将内容解码为Flux<Part>不收集到MultiValueMap.

转发的标头

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

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

ForwardedHeaderTransformer是一个组件,它根据转发的标头修改请求的主机、端口和方案,然后删除这些标头。如果将其声明为带有 name 的 bean forwardedHeaderTransformer,它将被 检测并使用。

转发的标头有安全考虑,因为应用程序无法知道标头是由代理添加的、按预期添加的,还是由恶意客户端添加的。这就是为什么应配置信任边界的代理以删除来自外部的不受信任的转发流量。您还可以配置ForwardedHeaderTransformerwith removeOnly=true,在这种情况下,它会删除但不使用标题。

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

1.2.3。过滤器

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

CORS

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

有关更多详细信息,请参阅有关CORSCORSWebFilter的部分。

1.2.4。例外

WebHandlerAPI中,您可以使用 a来处理来自实例WebExceptionHandler链和目标的异常。使用 WebFlux Config时,注册 a就像将其声明为 Spring bean 并(可选地)通过在 bean 声明上使用或通过实现来表示优先级一样简单。WebFilterWebHandlerWebExceptionHandler@OrderOrdered

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

异常处理程序 描述

ResponseStatusExceptionHandler

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

WebFluxResponseStatusExceptionHandler

它的扩展ResponseStatusExceptionHandler还可以确定@ResponseStatus任何异常的注释的 HTTP 状态代码。

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

1.2.5。编解码器

和模块支持通过非阻塞 I/O 和 Reactive Streams 背压来序列化和反序列化字节内容到更高级别的对象spring-webspring-core下面描述了这种支持:

  • Encoder并且 Decoder是独立于 HTTP 对内容进行编码和解码的低级合约。

  • HttpMessageReader并且 HttpMessageWriter是对 HTTP 消息内容进行编码和解码的合约。

  • anEncoder可以用 包裹起来EncoderHttpMessageWriter以适应在 Web 应用程序中使用,而 aDecoder可以用DecoderHttpMessageReader.

  • DataBuffer抽象不同的字节缓冲区表示(例如 Netty ByteBufjava.nio.ByteBuffer等)并且是所有编解码器的工作。有关此主题的更多信息,请参阅“Spring Core”部分中的数据缓冲区和编解码器。

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

ClientCodecConfigurer并且ServerCodecConfigurer通常用于配置和自定义编解码器以在应用程序中使用。请参阅配置 HTTP 消息编解码器部分。

杰克逊 JSON

当存在 Jackson 库时, JSON 和二进制 JSON ( Smile ) 都受支持。

Jackson2Decoder作品如下:

  • Jackson 的异步、非阻塞解析器用于将字节块流聚合成TokenBuffer's,每个块代表一个 JSON 对象。

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

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

  • 当解码到多值发布者(例如Flux)时,一旦接收到足够的字节用于完全形成的对象,每个都TokenBuffer被传递给。ObjectMapper输入内容可以是 JSON 数组,或任何以 行分隔的 JSON格式,例如 NDJSON、JSON 行或 JSON 文本序列。

Jackson2Encoder作品如下:

  • 对于单值发布者(例如Mono),只需通过 ObjectMapper.

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

  • 对于具有流媒体类型(例如 application/x-ndjsonor )的多值发布者,使用行分隔的 JSON格式application/stream+x-jackson-smile单独编码、写入和刷新每个值 。其他流媒体类型可以注册到编码器。

  • 对于 SSE,Jackson2Encoder每个事件都会调用它并刷新输出以确保无延迟地交付。

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

表格数据

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

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

一旦getFormData()使用,就无法再从请求正文中读取原始原始内容。出于这个原因,应用程序应该ServerWebExchange 始终如一地访问缓存的表单数据,而不是从原始请求正文中读取。

多部分

MultipartHttpMessageReaderMultipartHttpMessageWriter支持解码和编码“multipart/form-data”内容。依次MultipartHttpMessageReader委托给另一个HttpMessageReader进行实际解析到 a Flux<Part>,然后简单地将部分收集到 aMultiValueMap中。默认情况下,DefaultPartHttpMessageReader使用 ,但这可以通过 ServerCodecConfigurer. 有关 的更多信息DefaultPartHttpMessageReader,请参阅 的 javadocDefaultPartHttpMessageReader

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

一旦getMultipartData()使用,就无法再从请求正文中读取原始原始内容。出于这个原因,应用程序必须始终如一地使用getMultipartData() 对部件的重复、类似地图的访问,或者以其他方式依赖于 SynchronossPartHttpMessageReaderFlux<Part>.

限制

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

要配置缓冲区大小,您可以检查给定DecoderHttpMessageReader 公开maxInMemorySize属性,如果是,Javadoc 将包含有关默认值的详细信息。在服务器端,ServerCodecConfigurer提供了一个设置所有编解码器的位置,请参阅HTTP 消息编解码器。在客户端,可以在 WebClient.Builder中更改所有编解码器的限制。

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

流媒体

当流式传输到 HTTP 响应(例如 , text/event-streamapplication/x-ndjson时,定期发送数据很重要,以便尽早可靠地检测断开连接的客户端。这样的发送可以是仅评论、空 SSE 事件或任何其他可以有效用作心跳的“无操作”数据。

DataBuffer

DataBuffer是 WebFlux 中字节缓冲区的表示形式。本参考资料的 Spring Core 部分在 Data Buffers and Codecs部分有更多内容。要理解的关键点是,在一些像 Netty 这样的服务器上,字节缓冲区是池化和引用计数的,并且必须在消耗时释放以避免内存泄漏。

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

1.2.6。日志记录

DEBUGSpring WebFlux 中的级别日志记录设计为紧凑、最小且人性化。它侧重于反复有用的高价值信息,而不是仅在调试特定问题时有用的信息。

TRACE级别日志记录通常遵循相同的原则DEBUG(例如也不应该是消防软管),但可用于调试任何问题。此外,某些日志消息可能会在TRACEvs处显示不同级别的详细信息DEBUG

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

日志 ID

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

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

敏感数据

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

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

java
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().enableLoggingRequestDetails(true);
    }
}
科特林
@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {

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

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

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

WebClient webClient = WebClient.builder()
        .exchangeStrategies(strategies -> strategies.codecs(consumer))
        .build();
科特林
val consumer: (ClientCodecConfigurer) -> Unit  = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }

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

SLF4J 和 Log4J 2 等日志库提供了避免阻塞的异步记录器。虽然这些有其自身的缺点,例如可能会丢弃无法排队等待记录的消息,但它们是目前用于反应式、非阻塞应用程序的最佳可用选项。

自定义编解码器

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

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

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

java
WebClient webClient = WebClient.builder()
        .codecs(configurer -> {
                CustomDecoder decoder = new CustomDecoder();
                configurer.customCodecs().registerWithDefaultConfig(decoder);
        })
        .build();
科特林
val webClient = WebClient.builder()
        .codecs({ configurer ->
                val decoder = CustomDecoder()
                configurer.customCodecs().registerWithDefaultConfig(decoder)
         })
        .build()

1.3.DispatcherHandler

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

DispatcherHandler从 Spring 配置中发现它需要的委托组件。它也被设计成一个 Spring bean 本身并实现ApplicationContextAware 访问它运行的上下文。如果DispatcherHandler用 bean 名称声明webHandler,则它反过来由 发现 WebHttpHandlerBuilder,它将请求处理链组合在一起,如WebHandlerAPI中所述。

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

配置WebHttpHandlerBuilder用于构建处理链,如以下示例所示:

java
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
科特林
val context: ApplicationContext = ...
val handler = WebHttpHandlerBuilder.applicationContext(context).build()

结果HttpHandler可以与服务器适配器一起使用。

1.3.1。特殊豆类

特殊 bean的DispatcherHandler代表来处理请求并呈现适当的响应。“特殊 bean”是Object指实现 WebFlux 框架契约的 Spring 管理的实例。这些通常带有内置合同,但您可以自定义它们的属性、扩展它们或替换它们。

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

豆类 解释

HandlerMapping

将请求映射到处理程序。映射基于一些标准,其细节因HandlerMapping实现而异——带注释的控制器、简单的 URL 模式映射等。

主要HandlerMapping实现RequestMappingHandlerMapping用于 @RequestMapping注释方法、RouterFunctionMapping功能端点路由以及URI 路径模式和实例SimpleUrlHandlerMapping的显式注册。WebHandler

HandlerAdapter

帮助DispatcherHandler调用映射到请求的处理程序,而不管实际调用处理程序的方式。例如,调用带注释的控制器需要解析注释。a 的主要目的HandlerAdapter是屏蔽 DispatcherHandler这些细节。

HandlerResultHandler

处理处理程序调用的结果并最终确定响应。请参阅结果处理

1.3.2. WebFlux 配置

应用程序可以声明处理请求所需的基础设施 bean(列在 Web Handler API和 下)。DispatcherHandler然而,在大多数情况下,WebFlux Config是最好的起点。它声明所需的 bean 并提供更高级别的配置回调 API 来定制它。

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

1.3.3. 加工

DispatcherHandler处理请求如下:

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

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

  • 通过直接写入响应或使用视图进行呈现,将其提供给适当的完成处理HandlerResultHandlerResultHandler

1.3.4。结果处理

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

结果处理程序类型 返回值 默认顺序

ResponseEntityResultHandler

ResponseEntity,通常来自@Controller实例。

0

ServerResponseResultHandler

ServerResponse,通常来自功能端点。

0

ResponseBodyResultHandler

@ResponseBody处理来自方法或@RestController类的返回值。

100

ViewResolutionResultHandler

CharSequence, View, Model , Map, Rendering或任何其他Object被视为模型属性。

另请参阅查看分辨率

Integer.MAX_VALUE

1.3.5。例外

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

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

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

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

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

另请参阅“带注释的控制器”部分中的 管理异常或WebHandler API 部分中的异常。

1.3.6。查看分辨率

视图分辨率允许使用 HTML 模板和模型渲染到浏览器,而无需将您绑定到特定的视图技术。在 Spring WebFlux 中,视图解析是通过专用的HandlerResultHandler支持的,它使用 ViewResolver实例将 String(表示逻辑视图名称)映射到View 实例。然后View使用 来呈现响应。

处理

HandlerResult传入的ViewResolutionResultHandler包含来自处理程序的返回值和包含在请求处理期间添加的属性的模型。返回值被处理为下列之一:

  • String, :通过配置实现列表CharSequence解析为 a 的逻辑视图名称。ViewViewResolver

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

  • 渲染:用于视图解析场景的 API。通过代码完成探索 IDE 中的选项。

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

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

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

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

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

重定向

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

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

内容协商

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

为了支持 JSON 和 XML 等媒体类型,Spring WebFlux 提供 HttpMessageWriterView了一种特殊ViewHttpMessageWriter渲染。通常,您可以通过WebFlux 配置将它们配置为默认视图。如果默认视图与请求的媒体类型匹配,则始终选择并使用默认视图。

1.4. 带注释的控制器

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

以下清单显示了一个基本示例:

java
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String handle() {
        return "Hello WebFlux";
    }
}
科特林
@RestController
class HelloController {

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

在前面的示例中,该方法返回String要写入响应正文的 a。

1.4.1。@Controller

您可以使用标准 Spring bean 定义来定义控制器 bean。构造@Controller型允许自动检测,并与 Spring 对检测@Component类路径中的类并为它们自动注册 bean 定义的一般支持保持一致。它还充当带注释类的原型,表明其作为 Web 组件的角色。

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

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

    // ...
}
1 扫描org.example.web包裹。
科特林
@Configuration
@ComponentScan("org.example.web") (1)
class WebConfig {

    // ...
}
1 扫描org.example.web包裹。

@RestController是一个组合注解,它本身用@Controllerand进行元注解@ResponseBody,表示一个控制器,它的每个方法都继承了类型级别的@ResponseBody注解,因此,直接写入响应正文而不是视图分辨率并使用 HTML 模板进行渲染。

1.4.2. 请求映射

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

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

  • @GetMapping

  • @PostMapping

  • @PutMapping

  • @DeleteMapping

  • @PatchMapping

前面的注释是提供的自定义注释,因为可以说,大多数控制器方法应该映射到特定的 HTTP 方法而不是 using @RequestMapping,默认情况下,它匹配所有 HTTP 方法。同时, @RequestMapping在类级别仍然需要 a 来表示共享映射。

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

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

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

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        // ...
    }
}
科特林
@RestController
@RequestMapping("/persons")
class PersonController {

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

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

您可以使用 glob 模式和通配符映射请求:

图案 描述 例子

?

匹配一个字符

"/pages/t?st.html"比赛"/pages/test.html""/pages/t3st.html"

*

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

"/resources/*.png"火柴"/resources/file.png"

"/projects/*/versions"匹配"/projects/spring/versions"但不匹配"/projects/spring/boot/versions"

**

匹配零个或多个路径段,直到路径结束

"/resources/**"比赛"/resources/file.png""/resources/images/file.png"

"/resources/**/file.png"无效,因为**只允许在路径的末尾。

{name}

匹配路径段并将其捕获为名为“name”的变量

"/projects/{project}/versions"匹配"/projects/spring/versions"和捕获project=spring

{name:[a-z]+}

将正则表达式匹配"[a-z]+"为名为“name”的路径变量

"/projects/{project:[a-z]+}/versions"匹配"/projects/spring/versions"但不匹配"/projects/spring1/versions"

{*path}

匹配零个或多个路径段,直到路径结束并将其捕获为名为“path”的变量

"/resources/{*file}"匹配"/resources/images/file.png"和捕获file=/images/file.png

可以使用 访问捕获的 URI 变量@PathVariable,如以下示例所示:

java
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
    // ...
}
科特林
@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 映射。
科特林
@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 引发 a。默认支持简单类型(intlongDate等),您可以注册对任何其他数据类型的支持。请参阅类型转换DataBinder.

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

该语法{*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) {
    // ...
}
科特林
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable version: String, @PathVariable ext: String) {
    // ...
}

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

Spring WebFlux 使用PathPatternPathPatternParserfor 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) {
    // ...
}
科特林
@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) {
    // ...
}
科特林
@GetMapping("/pets/{petId}", produces = ["application/json"])
@ResponseBody
fun getPet(@PathVariable String petId): Pet {
    // ...
}

媒体类型可以指定一个字符集。支持否定表达式——例如, !text/plain表示除text/plain.

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

MediaType为常用媒体类型提供常量, APPLICATION_JSON_VALUE例如APPLICATION_XML_VALUE.
参数和标题

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

java
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
1 检查myParam等于myValue
科特林
@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
科特林
@GetMapping("/pets", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
    // ...
}
1 检查myHeader等于myValue
HTTP 头,选项

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

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

对于@RequestMapping没有 HTTP 方法声明的,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 方法,而不是 using @RequestMapping,默认情况下,它与所有 HTTP 方法匹配。如果您需要组合注释的示例,请查看它们是如何声明的。

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

显式注册

您可以通过编程方式注册 Handler 方法,这些方法可用于动态注册或高级情况,例如不同 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 添加注册。
科特林
@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处理程序方法具有灵活的签名,可以从一系列受支持的控制器方法参数和返回值中进行选择。

方法参数

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

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

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

控制器方法参数 描述

ServerWebExchange

访问完整ServerWebExchange 的——HTTP 请求和响应、请求和会话属性、checkNotModified方法等的容器。

ServerHttpRequest,ServerHttpResponse

访问 HTTP 请求或响应。

WebSession

访问会话。除非添加了属性,否则这不会强制启动新会话。支持反应类型。

java.security.Principal

Principal当前经过身份验证的用户——如果已知,可能是特定的实现类。支持反应类型。

org.springframework.http.HttpMethod

请求的 HTTP 方法。

java.util.Locale

当前请求区域设置,由最具体的LocaleResolver可用区域确定——实际上是配置的LocaleResolver/ LocaleContextResolver

java.util.TimeZone+java.time.ZoneId

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

@PathVariable

用于访问 URI 模板变量。请参阅URI 模式

@MatrixVariable

用于访问 URI 路径段中的名称-值对。请参阅矩阵变量

@RequestParam

用于访问 Servlet 请求参数。参数值被转换为声明的方法参数类型。见@RequestParam

请注意,使用 of@RequestParam是可选的——例如,设置其属性。请参阅本表后面的“任何其他参数”。

@RequestHeader

用于访问请求标头。标头值被转换为声明的方法参数类型。见@RequestHeader

@CookieValue

用于访问 cookie。Cookie 值被转换为声明的方法参数类型。见@CookieValue

@RequestBody

用于访问 HTTP 请求正文。HttpMessageReader使用实例将正文内容转换为声明的方法参数类型。支持反应类型。见@RequestBody

HttpEntity<B>

用于访问请求标头和正文。正文与HttpMessageReader实例一起转换。支持反应类型。见HttpEntity

@RequestPart

用于访问multipart/form-data请求中的一部分。支持反应类型。请参阅多部分内容多部分数据

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

用于访问 HTML 控制器中使用的模型,并作为视图呈现的一部分向模板公开。

@ModelAttribute

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

请注意,使用 of@ModelAttribute是可选的——例如,设置其属性。请参阅本表后面的“任何其他参数”。

Errors,BindingResult

用于访问来自命令对象(即 @ModelAttribute参数)的验证和数据绑定的错误。一个Errors, orBindingResult参数必须在验证的方法参数之后立即声明。

SessionStatus+ 班级级别@SessionAttributes

用于标记表单处理完成,这会触发对通过类级别@SessionAttributes注释声明的会话属性的清理。有关@SessionAttributes更多详细信息,请参阅。

UriComponentsBuilder

用于准备相对于当前请求的主机、端口、方案和上下文路径的 URL。请参阅URI 链接

@SessionAttribute

用于访问任何会话属性——与作为类级@SessionAttributes声明的结果存储在会话中的模型属性相反。有关 @SessionAttribute更多详细信息,请参阅。

@RequestAttribute

用于访问请求属性。有关@RequestAttribute更多详细信息,请参阅。

任何其他论点

如果方法参数与上述任何一个都不匹配,则默认情况下,@RequestParam如果它是简单类型,则将其解析为 a ,由 BeanUtils#isSimpleProperty确定,否则解析为 a @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是可选的。请参阅本表后面的“任何其他返回值”。

Rendering

用于模型和视图渲染场景的 API。

void

一个void可能是异步的(例如,Mono<Void>),返回类型(或null返回值)的方法被认为已经完全处理了响应,如果它也有一个ServerHttpResponse,一个ServerWebExchange参数,或者一个@ResponseStatus注解。lastModified如果控制器进行了肯定的 ETag 或时间戳检查,情况也是如此。// TODO:请参阅控制器了解详细信息。

如果以上都不成立,则void返回类型还可以指示 REST 控制器的“无响应主体”或 HTML 控制器的默认视图名称选择。

Flux<ServerSentEvent>, Observable<ServerSentEvent>, 或其他反应类型

发出服务器发送的事件。当ServerSentEvent只需要写入数据时,可以省略包装器(但是text/event-stream必须通过produces属性在映射中请求或声明)。

任何其他返回值

如果返回值与上述任何一项都不匹配,则默认情况下,将其视为视图名称,如果是Stringvoid(默认视图名称选择适用),或者作为要添加到模型的模型属性,除非它是一个简单类型,由 BeanUtils#isSimpleProperty确定,在这种情况下它仍然无法解析。

类型转换

如果参数被声明为非@RequestParam. _ @RequestHeader@PathVariable@MatrixVariable@CookieValueString

对于这种情况,将根据配置的转换器自动应用类型转换。默认情况下,支持简单类型(例如intlongDate等)。类型转换可以通过WebDataBinder(参见DataBinder)或通过注册 FormattersFormattingConversionService参见Spring Field Formatting)来定制。

类型转换中的一个实际问题是空字符串源值的处理。如果此类值是null类型转换的结果,则将其视为缺失。这可能是 、 和其他目标类型的Long情况UUID。如果要允许null 注入,请在参数注释上使用required标志,或将参数声明为@Nullable.

矩阵变量

RFC 3986讨论了路径段中的名称-值对。在 Spring WebFlux 中,我们根据 Tim Berners-Lee 的 “旧帖子”将它们称为“矩阵变量” ,但它们也可以称为 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
}
科特林
// 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
}
科特林
@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
}
科特林
// GET /pets/42

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

    // q == 1
}

要获取所有矩阵变量,请使用 a 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]
}
科特林
// 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.
科特林
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仅绑定到查询参数,但您可以使用数据绑定将查询参数、表单数据和多部分应用到 命令对象

默认情况下,使用注解的方法参数是必需的,但您可以通过设置 a 的 required 标志或使用 包装器声明参数 来@RequestParam指定方法参数是可选的。@RequestParamfalsejava.util.Optional

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

在or 参数@RequestParam上声明注释时,映射将填充所有查询参数。Map<String, String>MultiValueMap<String, String>

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

@RequestHeader

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

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

主机本地主机:8080
接受 text/html,application/xhtml+xml,application/xml;q=0.9
接受语言 fr,en-gb;q=0.7,en;q=0.3
接受编码 gzip,deflate
接受字符集 ISO-8859-1,utf-8;q=0.7,*;q=0.7
保活300

以下示例获取Accept-EncodingKeep-Alive标头的值:

java
@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, (1)
        @RequestHeader("Keep-Alive") long keepAlive) { (2)
    //...
}
1 获取Accept-Encoging标头的值。
2 获取Keep-Alive标头的值。
科特林
@GetMapping("/demo")
fun handle(
        @RequestHeader("Accept-Encoding") encoding: String, (1)
        @RequestHeader("Keep-Alive") keepAlive: Long) { (2)
    //...
}
1 获取Accept-Encoging标头的值。
2 获取Keep-Alive标头的值。

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

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

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

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

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

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

以下代码示例演示了如何获取 cookie 值:

java
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
    //...
}
1 获取 cookie 值。
科特林
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
    //...
}
1 获取 cookie 值。

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

@ModelAttribute

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

java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
1 绑定一个实例Pet
科特林
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } (1)
1 绑定一个实例Pet

上例中的Pet实例解析如下:

  • 如果已经通过Model.

  • 从 HTTP 会话到@SessionAttributes.

  • 通过调用默认构造函数。

  • 通过使用与查询参数或表单字段匹配的参数调用“主构造函数”。参数名称通过 JavaBean @ConstructorProperties或字节码中运行时保留的参数名称确定。

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

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

java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}
1 添加一个BindingResult.
科特林
@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 验证)。以下示例使用@Valid注释:

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

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

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

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

@SessionAttributes

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

考虑以下示例:

java
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
    // ...
}
1 使用@SessionAttributes注释。
科特林
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
    // ...
}
1 使用@SessionAttributes注释。

在第一次请求时,当一个名为 , 的模型属性pet被添加到模型中时,它会自动提升并保存在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变量。
科特林
@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

如果您需要访问全局管理的预先存在的会话属性(即,在控制器外部 - 例如,通过过滤器)并且可能存在也可能不存在,您可以@SessionAttribute在方法参数上使用注释,如下所示示例显示:

java
@GetMapping("/")
public String handle(@SessionAttribute User user) { (1)
    // ...
}
1 使用@SessionAttribute.
科特林
@GetMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
    // ...
}
1 使用@SessionAttribute.

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

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

@RequestAttribute

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

java
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
    // ...
}
1 使用@RequestAttribute.
科特林
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
    // ...
}
1 使用@RequestAttribute.
多部分内容

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

java
class MyForm {

    private String name;

    private MultipartFile file;

    // ...

}

@Controller
public class FileUploadController {

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

}
科特林
class MyForm(
        val name: String,
        val file: MultipartFile)

@Controller
class FileUploadController {

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

}

您还可以在 RESTful 服务场景中提交来自非浏览器客户端的多部分请求。以下示例使用文件和 JSON:

发布 /someUrl
内容类型:多部分/混合

--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
内容处置:表单数据;名称="元数据"
内容类型:应用程序/json;字符集=UTF-8
内容传输编码:8bit

{
    “名称”:“价值”
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
内容处置:表单数据;名称="文件数据"; 文件名="文件.properties"
内容类型:文本/xml
内容传输编码:8bit
... 文件数据 ...

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

java
@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, (1)
        @RequestPart("file-data") FilePart file) { (2)
    // ...
}
1 用于@RequestPart获取元数据。
2 用于@RequestPart获取文件。
科特林
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, (1)
        @RequestPart("file-data") FilePart file): String { (2)
    // ...
}
1 用于@RequestPart获取元数据。
2 用于@RequestPart获取文件。

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

java
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
    // ...
}
1 用于@RequestPart获取元数据。
科特林
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
    // ...
}
1 用于@RequestPart获取元数据。

您可以与Spring 的 注解@RequestPart结合使用,这会导致应用标准 Bean 验证。验证错误会导致400 (BAD_REQUEST) 响应。异常包含带有错误详细信息的异常,也可以通过使用异步包装器声明参数然后使用与错误相关的运算符在控制器方法中处理:javax.validation.Valid@ValidatedWebExchangeBindExceptionBindingResult

java
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
    // use one of the onError* operators...
}
科特林
@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.
科特林
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
    // ...
}
1 使用@RequestBody.

要以流方式顺序访问多部分数据,您可以改用@RequestBodywith Flux<Part>(或Flow<Part>在 Kotlin 中),如以下示例所示:

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

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

java
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
    // ...
}
科特林
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
    // ...
}

与 Spring MVC 不同,在 WebFlux 中,@RequestBody方法参数支持响应式类型和完全非阻塞读取和(客户端到服务器)流。

java
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
    // ...
}
科特林
@PostMapping("/accounts")
fun handle(@RequestBody accounts: Flow<Account>) {
    // ...
}

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

您可以与Spring 的 注解@RequestBody结合使用,这会导致应用标准 Bean 验证。验证错误会导致400 (BAD_REQUEST) 响应。该异常包含带有错误详细信息的异常,可以通过使用异步包装器声明参数然后使用与错误相关的运算符在控制器方法中进行处理:javax.validation.Valid@ValidatedWebExchangeBindExceptionBindingResult

java
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Mono<Account> account) {
    // use one of the onError* operators...
}
科特林
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Mono<Account>) {
    // ...
}
HttpEntity

HttpEntity或多或少与 using 相同,@RequestBody但基于公开请求标头和正文的容器对象。以下示例使用 HttpEntity:

java
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
    // ...
}
科特林
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
    // ...
}
@ResponseBody

您可以使用@ResponseBody方法上的注释通过HttpMessageWriter将返回序列化到响应正文。以下示例显示了如何执行此操作:

java
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
    // ...
}
科特林
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
    // ...
}

@ResponseBody在类级别也支持,在这种情况下,它被所有控制器方法继承。这就是 的效果@RestController,只不过是一个用@Controllerand标记的元注释@ResponseBody

@ResponseBody支持响应式类型,这意味着您可以返回 Reactor 或 RxJava 类型并将它们生成的异步值呈现给响应。有关其他详细信息,请参阅流式处理JSON 呈现

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

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

ResponseEntity

ResponseEntity就像@ResponseBody但有状态和标题。例如:

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

WebFlux 支持使用单值响应类型为主体生成ResponseEntity异步和/或单值和多值响应类型。这允许各种异步响应,ResponseEntity如下所示:

  • ResponseEntity<Mono<T>>或者ResponseEntity<Flux<T>>在稍后异步提供正文时立即知道响应状态和标头。如果Mono主体由 0..1 个值组成,或者Flux它可以产生多个值,则使用。

  • Mono<ResponseEntity<T>>提供所有三个——响应状态、标头和正文,稍后异步提供。这允许响应状态和标头根据异步请求处理的结果而变化。

  • Mono<ResponseEntity<Mono<T>>>或者Mono<ResponseEntity<Flux<T>>>是另一种可能的选择,尽管不太常见。它们首先异步提供响应状态和标头,然后是响应正文,也是异步的,其次。

杰克逊 JSON

Spring 提供对 Jackson JSON 库的支持。

JSON 视图

Spring WebFlux 提供了对Jackson 的序列化视图的内置支持 ,它允许只渲染Object. 要将其与 @ResponseBody控制器ResponseEntity方法一起使用,您可以使用 Jackson 的 @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;
    }
}
科特林
@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上创建或访问模型中的对象并通过 WebDataBinder.

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

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

本节讨论@ModelAttribute方法,或前面列表中的第二项。一个控制器可以有任意数量的@ModelAttribute方法。所有此类方法都@RequestMapping在同一控制器中的方法之前调用。一个@ModelAttribute 方法也可以通过@ControllerAdvice. 有关详细信息,请参阅 控制器建议部分。

@ModelAttribute方法具有灵活的方法签名。它们支持许多与方法相同的参数@RequestMapping(除了@ModelAttribute自身和与请求正文相关的任何内容)。

下面的例子使用了一个@ModelAttribute方法:

java
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountRepository.findAccount(number));
    // add more ...
}
科特林
@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);
}
科特林
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
    return accountRepository.findAccount(number);
}
如果未明确指定名称,则会根据类型选择默认名称,如Conventions. 您始终可以通过使用重载addAttribute方法或通过 name 属性@ModelAttribute(用于返回值)来分配显式名称。

Spring WebFlux 与 Spring MVC 不同,在模型中显式支持响应式类型(例如,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) {
    // ...
}
科特林
import org.springframework.ui.set

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

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

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

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

java
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
    // ...
    return account;
}
科特林
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
    // ...
    return account
}

1.4.5。DataBinder

@Controller或者@ControllerAdvice类可以有@InitBinder方法来初始化WebDataBinder. 反过来,这些用于:

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

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

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

@InitBinder方法可以注册特定于控制器java.beans.PropertyEditor或 SpringConverterFormatter组件。此外,您可以使用 WebFlux Java 配置来注册ConverterFormatter键入全局共享的FormattingConversionService.

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

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注释。
科特林
@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通过 shared 使用基于 FormattingConversionService- 的设置时,您可以重复使用相同的方法并注册特定于控制器的Formatter实例,如以下示例所示:

java
@Controller
public class FormController {

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

    // ...
}
1 添加自定义格式化程序(DateFormatter在本例中为 a )。
科特林
@Controller
class FormController {

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

    // ...
}
1 添加自定义格式化程序(DateFormatter在本例中为 a )。
模型设计

在 Web 应用程序的上下文中,数据绑定涉及将 HTTP 请求参数(即表单数据或查询参数)绑定到模型对象及其嵌套对象中的属性。

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

模型对象及其嵌套对象图有时也称为 命令对象表单支持对象POJO(普通旧 Java 对象)。

默认情况下,Spring 允许绑定到模型对象图中的所有公共属性。这意味着您需要仔细考虑模型具有哪些公共属性,因为客户端可以针对任何公共属性路径,甚至一些不希望针对给定用例的公共属性路径。

例如,给定一个 HTTP 表单数据端点,恶意客户端可以为模型对象图中存在但不属于浏览器中呈现的 HTML 表单的一部分的属性提供值。这可能导致在模型对象及其任何嵌套对象上设置数据,预计不会更新。

推荐的方法是使用一个专门的模型对象,它只公开与表单提交相关的属性。例如,在用于更改用户电子邮件地址的表单上,模型对象应声明一组最小属性,如下所示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;
    }

}

如果您不能或不想为每个数据绑定用例使用专用模型对象,则必须限制数据绑定允许的属性。理想情况下,您可以通过 on 方法注册允许的字段模式来实现此目的。setAllowedFields()WebDataBinder

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

@Controller
public class ChangeEmailController {

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

    // @RequestMapping methods, etc.

}

除了注册允许的模式外,还可以通过in及其子类的方法注册不允许的字段模式。但是请注意,“允许列表”比“拒绝列表”更安全。因此, 应优先考虑。setDisallowedFields()DataBindersetAllowedFields()setDisallowedFields()

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

在直接公开域模型以进行数据绑定时,正确配置允许和不允许的字段模式非常重要。否则,就是很大的安全隐患。

此外,强烈建议您不要域模型中的类型(例如 JPA 或 Hibernate 实体)用作数据绑定场景中的模型对象。

1.4.6。管理异常

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

java
@Controller
public class SimpleController {

    // ...

    @ExceptionHandler (1)
    public ResponseEntity<String> handle(IOException ex) {
        // ...
    }
}
1 声明一个@ExceptionHandler.
科特林
@Controller
class SimpleController {

    // ...

    @ExceptionHandler (1)
    fun handle(ex: IOException): ResponseEntity<String> {
        // ...
    }
}
1 声明一个@ExceptionHandler.

异常可以匹配正在传播的顶级异常(即直接 IOException抛出)或顶级包装异常中的直接原因(例如,IOException包装在 中IllegalStateException)。

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

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

@ExceptionHandlerSpring WebFlux 中 的方法支持由HandlerAdapterfor@RequestMapping方法提供。有关DispatcherHandler 更多详细信息,请参阅。

REST API 异常

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

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

1.4.7。控制器建议

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

@ControllerAdvice注释为@Component,表示此类类可以通过组件扫描注册为Spring bean 。@RestControllerAdvice是一个组合注解,它同时使用@ControllerAdvice和进行注解@ResponseBody,这本质上意味着 @ExceptionHandler方法通过消息转换(相对于视图解析或模板呈现)呈现到响应主体。

在启动时,基础设施类@RequestMapping@ExceptionHandler 方法检测带有注释的 Spring bean @ControllerAdvice,然后在运行时应用它们的方法。全局@ExceptionHandler方法(来自 a @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 {}
科特林
// 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 包含 WebFlux.fn,这是一个轻量级的函数式编程模型,其中函数用于路由和处理请求,并且为不可变而设计了合约。它是基于注释的编程模型的替代方案,但在相同的Reactive Core基础上运行。

1.5.1。概述

在 WebFlux.fn 中,HTTP 请求使用HandlerFunction: 处理,该函数接受 ServerRequest并返回延迟ServerResponse(即Mono<ServerResponse>)。请求和响应对象都有不可变的契约,提供对 HTTP 请求和响应的 JDK 8 友好访问。 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) {
        // ...
    }
}
科特林
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 使用 Coroutines 路由器 DSL 创建路由器,也可以通过router { }.

运行 a 的一种方法RouterFunction是将其转换为 anHttpHandler并通过内置服务器适配器之一安装它:

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

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

1.5.2. 处理函数

ServerRequest并且ServerResponse是不可变接口,提供对 HTTP 请求和响应的 JDK 8 友好访问。请求和响应都为响应式流提供针对主体流的背压。请求正文用 ReactorFluxMono. 响应主体由任何 Reactive Streams 表示Publisher,包括FluxMono。有关更多信息,请参阅反应式库

服务器请求

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

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

java
Mono<String> string = request.bodyToMono(String.class);
科特林
val string = request.awaitBody<String>()

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

java
Flux<Person> people = request.bodyToFlux(Person.class);
科特林
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));
科特林
    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();
科特林
val map = request.awaitFormData()

以下示例显示了如何以地图的形式访问多部分数据:

java
Mono<MultiValueMap<String, Part>> map = request.multipartData();
科特林
val map = request.awaitMultipartData()

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

java
Flux<Part> parts = request.body(BodyExtractors.toParts());
科特林
val parts = request.body(BodyExtractors.toParts()).asFlow()
服务器响应

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

java
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
科特林
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)

以下示例显示了如何构建带有Location标头但没有正文的 201 (CREATED) 响应:

java
URI location = ...
ServerResponse.created(location).build();
科特林
val location: URI = ...
ServerResponse.created(location).build()

根据使用的编解码器,可以传递提示参数来自定义正文如何序列化或反序列化。例如,要指定Jackson JSON 视图

java
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
科特林
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
处理程序类

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

java
HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().bodyValue("Hello World");
科特林
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }

这很方便,但是在应用程序中我们需要多个函数,并且多个内联 lambda 会变得混乱。因此,将相关的处理程序函数组合到一个处理程序类中是很有用的,该处理程序类的作用与@Controller基于注释的应用程序中的作用相似。例如,以下类公开了一个反应式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是一个处理函数,它Person以 JSON 形式返回在存储库中找到的所有对象。
2 createPerson是一个处理函数,用于存储Person请求正文中包含的新内容。请注意,当从请求中读取并存储人员时,PersonRepository.savePerson(Person)返回Mono<Void>:一个空的Mono,它会发出完成信号。因此,我们使用该 方法在接收到完成信号时(即已保存时)build(Publisher<Void>)发送响应。Person
3 getPerson是一个处理函数,它返回一个由id路径变量标识的人。我们Person从存储库中检索它并创建一个 JSON 响应(如果找到)。如果未找到,我们使用switchIfEmpty(Mono<T>)返回 404 Not Found 响应。
科特林
class PersonHandler(private val repository: PersonRepository) {

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

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

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

    }
}
1 listPeople是一个处理函数,它Person以 JSON 形式返回在存储库中找到的所有对象。
2 createPerson是一个处理函数,用于存储Person请求正文中包含的新内容。请注意,这PersonRepository.savePerson(Person)是一个没有返回类型的挂起函数。
3 getPerson是一个处理函数,它返回一个由id路径变量标识的人。我们Person从存储库中检索它并创建一个 JSON 响应(如果找到)。如果没有找到,我们返回 404 Not Found 响应。
验证

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

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 应用验证。
3 为 400 响应引发异常。
科特林
class PersonHandler(private val repository: PersonRepository) {

    private val validator = PersonValidator() (1)

    // ...

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

    private fun validate(person: Person) {
        val errors: Errors = BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw ServerWebInputException(errors.toString()) (3)
        }
    }
}
1 创建Validator实例。
2 应用验证。
3 为 400 响应引发异常。

处理程序还可以通过创建和注入Validator基于LocalValidatorFactoryBean. 请参阅Spring 验证

1.5.3.RouterFunction

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

通常,建议使用route()构建器,因为它为典型的映射场景提供了方便的快捷方式,而无需难以发现的静态导入。例如,路由器函数构建器提供了GET(String, HandlerFunction)为 GET 请求创建映射的方法;和POST(String, HandlerFunction)帖子。

除了基于 HTTP 方法的映射之外,路由构建器还提供了一种在映射到请求时引入额外谓词的方法。对于每个 HTTP 方法,都有一个以 aRequestPredicate作为参数的重载变体,尽管可以表达额外的约束。

谓词

您可以编写自己的RequestPredicate,但RequestPredicates实用程序类提供了常用的实现,基于请求路径、HTTP 方法、内容类型等。以下示例使用请求谓词基于Accept 标头创建约束:

java
RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
        request -> ServerResponse.ok().bodyValue("Hello World")).build();
科特林
val route = coRouter {
    GET("/hello-world", accept(TEXT_PLAIN)) {
        ServerResponse.ok().bodyValueAndAwait("Hello World")
    }
}

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

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

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

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

路线

路由器功能按顺序评估:如果第一个路由不匹配,则评估第二个路由,依此类推。因此,在一般路由之前声明更具体的路由是有意义的。这在将路由器功能注册为 Spring bean 时也很重要,稍后将进行描述。请注意,此行为与基于注释的编程模型不同,后者自动选择“最具体”的控制器方法。

使用路由函数构建器时,所有定义的路由都组合成一个 RouterFunctionbuild(). 还有其他方法可以将多个路由器功能组合在一起:

  • add(RouterFunction)RouterFunctions.route()建设者上

  • RouterFunction.and(RouterFunction)

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

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

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 GET /person/{id}与 JSON 匹配的Accept标头被路由到 PersonHandler.getPerson
2 GET /person与 JSON 匹配的Accept标头被路由到 PersonHandler.listPeople
3 POST /person没有额外的谓词被映射到 PersonHandler.createPerson, 和
4 otherRoute是在其他地方创建的路由函数,并添加到构建的路由中。
科特林
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 GET /person/{id}与 JSON 匹配的Accept标头被路由到 PersonHandler.getPerson
2 GET /person与 JSON 匹配的Accept标头被路由到 PersonHandler.listPeople
3 POST /person没有额外的谓词被映射到 PersonHandler.createPerson, 和
4 otherRoute是在其他地方创建的路由函数,并添加到构建的路由中。
嵌套路由

一组路由器功能通常具有共享谓词,例如共享路径。在上面的示例中,共享谓词将是匹配的路径谓词,/person由三个路由使用。使用注解时,您可以使用@RequestMapping映射到 /person. 在 WebFlux.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是使用路由器构建器的消费者。
科特林
val route = coRouter {
    "/person".nest {
        GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        GET(accept(APPLICATION_JSON), handler::listPeople)
        POST(handler::createPerson)
    }
}

尽管基于路径的嵌套是最常见的,但您可以使用nest构建器上的方法嵌套在任何类型的谓词上。上面仍然包含一些以 shared 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();
科特林
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 ConfigDispatcherHandler使用基于 - 的设置 运行,它使用 Spring 配置来声明处理请求所需的组件。WebFlux Java 配置声明了以下基础设施组件以支持功能端点:

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

  • HandlerFunctionAdapter:简单的适配器,可以DispatcherHandler调用HandlerFunction映射到请求的 a。

  • ServerResponseResultHandler: HandlerFunction通过调用 的writeTo方法来处理调用 a 的结果ServerResponse

前面的组件让功能端点适合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...
    }
}
科特林
@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过滤after处理函数。使用注释,您可以通过使用、 a或同时使用两者filter来实现类似的功能。过滤器将应用于构建器构建的所有路由。这意味着嵌套路由中定义的过滤器不适用于“顶级”路由。例如,考虑以下示例:@ControllerAdviceServletFilter

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过滤器适用于所有路由,包括嵌套路由。
科特林
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路由器构建器上的方法接受一个 : 一个接受 a并返回 aHandlerFilterFunction的函数。处理函数参数表示链中的下一个元素。这通常是路由到的处理程序,但如果应用多个,它也可以是另一个过滤器。ServerRequestHandlerFunctionServerResponse

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

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

1.6. URI 链接

本节介绍 Spring Framework 中用于准备 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.
科特林
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();
科特林
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");
科特林
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");
科特林
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123")

1.6.2. UriBuilder

Spring MVC 和 Spring WebFlux

UriComponentsBuilder实现UriBuilderUriBuilder反过来,您可以 使用UriBuilderFactory. 一起,UriBuilderFactoryUriBuilder提供一种可插入的机制,以基于共享配置(例如基本 URL、编码首选项和其他详细信息)从 URI 模板构建 URI。

您可以配置RestTemplateWebClient用 aUriBuilderFactory 来自定义编写 URI。DefaultUriBuilderFactory是内部UriBuilderFactory使用UriComponentsBuilder并公开共享配置选项的默认实现。

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

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

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

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
科特林
// 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();
科特林
// 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");
科特林
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)

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

1.6.3. URI 编码

Spring MVC 和 Spring WebFlux

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

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

考虑“;”,它在路径中是合法的,但具有保留的含义。第一个选项替换“;” 在 URI 变量中使用“%3B”,但在 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"
科特林
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");
科特林
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");
科特林
val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
        .build("New York", "foo+bar")

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

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();
科特林
val baseUrl = "https://example.com"
val factory = DefaultUriBuilderFactory(baseUrl).apply {
    encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}

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

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

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

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

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

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

  • NONE: 不应用编码。

RestTemplate设置为出于历史EncodingMode.URI_COMPONENT原因和向后兼容性。WebClient依赖于 中的默认值,该值DefaultUriBuilderFactoryEncodingMode.URI_COMPONENT5.0.x更改为EncodingMode.TEMPLATE_AND_VALUES5.1。

1.7. CORS

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

1.7.1。介绍

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

跨域资源共享 (CORS) 是 由大多数浏览器实现的W3C 规范,它允许您指定授权哪种类型的跨域请求,而不是使用基于 IFRAME 或 JSONP 的不太安全且功能较弱的解决方法。

1.7.2. 加工

CORS 规范区分了预检请求、简单请求和实际请求。要了解 CORS 的工作原理,您可以阅读 本文等,或查看规范以了解更多详细信息。

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

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

每个HandlerMapping都可以 使用基于 URL 模式的映射单独配置。CorsConfiguration在大多数情况下,应用程序使用 WebFlux Java 配置来声明此类映射,这会导致将单个全局映射传递给所有HandlerMapping实现。

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

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

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

  • CorsConfiguration

  • CorsProcessorDefaultCorsProcessor

  • 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) {
        // ...
    }
}
科特林
@RestController
@RequestMapping("/account")
class AccountController {

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

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

默认情况下,@CrossOrigin允许:

  • 所有的起源。

  • 所有标题。

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

allowCredentials默认情况下不启用,因为这会建立一个信任级别,该级别会公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌),并且只能在适当的情况下使用。启用时,allowOrigins必须将其设置为一个或多个特定域(但不是特殊值"*"),或者该allowOriginPatterns属性可用于匹配一组动态的来源。

maxAge设置为 30 分钟。

@CrossOrigin在类级别也受支持,并由所有方法继承。以下示例指定某个域并设置maxAge为一小时:

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) {
        // ...
    }
}
科特林
@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在方法级别使用。
科特林
@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 配置。您可以CorsConfiguration 在任何HandlerMapping. 然而,大多数应用程序使用 WebFlux Java 配置来执行此操作。

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

  • 所有的起源。

  • 所有标题。

  • GET, HEAD, 和POST方法。

allowedCredentials默认情况下不启用,因为这会建立一个信任级别,该级别会公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌),并且应该仅在适当的情况下使用。启用时,allowOrigins必须将其设置为一个或多个特定域(但不是特殊值"*"),或者该allowOriginPatterns属性可用于匹配一组动态的来源。

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...
    }
}
科特林
@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。CORSWebFilter

您可以通过内置应用 CORS 支持 CorsWebFilter,非常适合功能端点

如果您尝试使用CorsFilterSpring Security,请记住 Spring Security内置了对 CORS 的支持。

要配置过滤器,您可以声明一个CorsWebFilterbean 并将 a 传递 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);
}
科特林
@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. 网络安全

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

1.9。查看技术

Spring WebFlux 中视图技术的使用是可插拔的。无论您决定使用 Thymeleaf、FreeMarker 还是其他一些视图技术,主要取决于配置更改。本章介绍了与 Spring WebFlux 集成的视图技术。我们假设您已经熟悉View Resolution

1.9.1。百里香叶

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

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

1.9.2。自由标记

Apache FreeMarker是一个模板引擎,用于生成从 HTML 到电子邮件等的任何类型的文本输出。Spring 框架内置了将 Spring WebFlux 与 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/freemarker");
        return configurer;
    }
}
科特林
@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 配置

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

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;
    }
}
科特林
@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/>元素。此元素主要让表单显示来自表单支持对象的值,并显示来自ValidatorWeb 或业务层的验证失败的结果。Spring 还支持 FreeMarker 中的相同功能,并带有额外的便利宏,用于自己生成表单输入元素。

绑定宏

FreeMarker 文件中维护了一组标准宏spring-webflux.jar,因此它们始终可用于适当配置的应用程序。

Spring 模板库中定义的一些宏被认为是内部的(私有的),但宏定义中不存在这样的作用域,使得所有宏对调用代码和用户模板可见。以下部分仅关注您需要从模板中直接调用的宏。如果您想直接查看宏代码,则调用该文件spring.ftl并在 org.springframework.web.reactive.result.view.freemarker包中。

有关绑定支持的更多详细信息,请参阅Spring MVC 的简单绑定

表单宏

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

1.9.3。脚本视图

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

脚本库 脚本引擎

车把

纳肖恩

胡子

纳肖恩

反应

纳肖恩

EJS

纳肖恩

再培训局

JRuby

字符串模板

杰通

Kotlin 脚本模板

科特林

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

你需要在你的类路径上有脚本引擎,其细节因脚本引擎而异:

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

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

  • 应将Jython添加为 Python 支持的依赖项。

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

您需要有脚本模板库。对 JavaScript 执行此操作的一种方法是通过WebJars

脚本模板

您可以声明一个ScriptTemplateConfigurerbean 来指定要使用的脚本引擎、要加载的脚本文件、调用什么函数来渲染模板等等。以下示例使用 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;
    }
}
科特林
@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 renderingContextRenderingContext 提供对应用程序上下文、语言环境、模板加载器和 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;
    }
}
科特林
@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
    }
}
当使用非线程安全脚本引擎和不是为并发设计的模板库(例如在 Nashorn 上运行的 Handlebars 或 React)时,需要 将该sharedEngine属性设置为。false在这种情况下,由于 此错误,需要 Java SE 8 更新 60,但通常建议在任何情况下使用最近的 Java SE 补丁版本。

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

var window = {};

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

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

查看 Spring Framework 单元测试、 Java资源,了解更多配置示例。

1.9.4。JSON 和 XML

出于内容协商的目的,根据客户端请求的内容类型,能够在使用 HTML 模板或其他格式(例如 JSON 或 XML)渲染模型之间进行交替是很有用的。为了支持这样做,Spring WebFlux 提供了HttpMessageWriterView,您可以使用它来插入来自 的任何可用 编解码器spring-web例如Jackson2JsonEncoderJackson2SmileEncoderJaxb2XmlEncoder

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

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

1.10。HTTP缓存

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

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

1.10.1。CacheControl

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

虽然RFC 7234Cache-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();
科特林
// 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。控制器

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

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);
}
科特林
@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 继续处理请求。
科特林
@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)。对于条件POSTPUTDELETE,您可以改为将响应设置为 412 (PRECONDITION_FAILED) 以防止并发修改。

1.10.3。静态资源

您应该使用带有Cache-Control条件响应标头的静态资源来提供最佳性能。请参阅配置静态资源部分。

1.11。WebFlux 配置

WebFlux Java 配置声明了使用带注释的控制器或功能端点处理请求所需的组件,并提供了一个 API 来自定义配置。这意味着您不需要了解 Java 配置创建的底层 bean。但是,如果您想了解它们,您可以在Special Bean TypesWebFluxConfigurationSupport中查看它们或阅读更多关于它们是什么的信息。

对于配置 API 中不可用的更高级自定义,您可以通过 高级配置模式获得对配置的完全控制。

1.11.1。启用 WebFlux 配置

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

java
@Configuration
@EnableWebFlux
public class WebConfig {
}
科特林
@Configuration
@EnableWebFlux
class WebConfig

前面的示例注册了许多 Spring WebFlux 基础设施 bean,并适应类路径上可用的依赖项——用于 JSON、XML 等。

1.11.2。WebFlux 配置 API

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

java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    // Implement configuration methods...
}
科特林
@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) {
        // ...
    }

}
科特林
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        // ...
    }
}

默认情况下,Spring WebFlux 在解析和格式化日期值时会考虑请求 Locale。这适用于日期表示为带有“输入”表单字段的字符串的表单。然而,对于“日期”和“时间”表单字段,浏览器使用 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);
    }
}
科特林
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        val registrar = DateTimeFormatterRegistrar()
        registrar.setUseIsoFormat(true)
        registrar.registerFormatters(registry)
    }
}
有关何时使用实现 的更多信息, 请参阅FormatterRegistrarSPI 和。FormattingConversionServiceFactoryBeanFormatterRegistrar

1.11.4。验证

默认情况下,如果类路径中存在Bean Validation(例如,Hibernate Validator),则将LocalValidatorFactoryBean 其注册为全局验证器,以便与方法参数一起使用@Valid@Validated@Controller方法参数上使用。

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

java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

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

}
科特林
@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());
    }

}
科特林
@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) {
        // ...
    }
}
科特林
@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) {
        configurer.defaultCodecs().maxInMemorySize(512 * 1024);
    }
}
科特林
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

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

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

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

如果在类路径中检测到以下知名模块,它还会自动注册它们:

1.11.7。查看解析器

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

java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // ...
    }
}
科特林
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

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

ViewResolverRegistry具有与 Spring 框架集成的视图技术的快捷方式。以下示例使用 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;
    }
}
科特林
@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);
    }
}
科特林
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        val resolver: ViewResolver = ...
        registry.viewResolver(resolver
    }
}

要支持内容协商并通过视图解析(HTML 除外)呈现其他格式,您可以根据实现配置一个或多个默认视图,该实现HttpMessageWriterView接受 来自. spring-web以下示例显示了如何执行此操作:

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

    // ...
}
科特林
@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相对路径用于查找和提供相对于/static类路径上的静态资源。资源的服务期限为一年,以确保最大限度地使用浏览器缓存并减少浏览器发出的 HTTP 请求。Last-Modified还评估标头,如果存在,则304返回状态代码。以下列表显示了示例:

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

}
科特林
@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。A ContentVersionStrategy(MD5 哈希)是一个不错的选择,但有一些明显的例外(例如与模块加载器一起使用的 JavaScript 资源)。

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

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("/**"));
    }

}
科特林
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
    }

}

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

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

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

WebJars也通过 当库出现在类路径上WebJarsResourceResolver时自动注册来 支持。org.webjars:webjars-locator-core解析器可以重写 URL 以包含 jar 的版本,还可以匹配没有版本的传入 URL——例如 from /jquery/jquery.min.jsto /jquery/1.2.0/jquery.min.js

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

1.11.9。路径匹配

您可以自定义与路径匹配相关的选项。有关各个选项的详细信息,请参阅 PathMatchConfigurerjavadoc。下面的例子展示了如何使用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));
    }
}
科特林
@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 不同,我们也建议摆脱对它的依赖。

1.11.10。WebSocket服务

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

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

java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public WebSocketService getWebSocketService() {
        TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
        strategy.setMaxSessionIdleTimeout(0L);
        return new HandshakeWebSocketService(strategy);
    }
}
科特林
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    @Override
    fun webSocketService(): WebSocketService {
        val strategy = TomcatRequestUpgradeStrategy().apply {
            setMaxSessionIdleTimeout(0L)
        }
        return HandshakeWebSocketService(strategy)
    }
}

1.11.11。高级配置模式

@EnableWebFlux进口DelegatingWebFluxConfiguration

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

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

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

java
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {

    // ...
}
科特林
@Configuration
class WebConfig : DelegatingWebFluxConfiguration {

    // ...
}

您可以将现有方法保留在 中WebConfig,但您现在也可以覆盖基类中的 bean 声明,并且WebMvcConfigurer在类路径上仍然有任意数量的其他实现。

1.12。HTTP/2

Reactor Netty、Tomcat、Jetty 和 Undertow 支持 HTTP/2。但是,有一些与服务器配置相关的注意事项。有关更多详细信息,请参阅 HTTP/2 wiki 页面

2.Web客户端

Spring WebFlux 包含一个执行 HTTP 请求的客户端。WebClient有一个基于 Reactor 的功能性、流畅的 API,请参阅Reactive Libraries,它支持异步逻辑的声明式组​​合,而无需处理线程或并发。它是完全非阻塞的,它支持流式传输,并且依赖于相同的编解码器,这些编解码器也用于在服务器端对请求和响应内容进行编码和解码。

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

2.1。配置

创建 a 的最简单方法WebClient是通过其中一种静态工厂方法:

  • WebClient.create()

  • WebClient.create(String baseUrl)

您还可以使用WebClient.builder()更多选项:

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

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

  • defaultHeader: 每个请求的标头。

  • defaultCookie:每个请求的 Cookie。

  • defaultRequestConsumer自定义每个请求。

  • filter:每个请求的客户端过滤器。

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

  • clientConnector:HTTP 客户端库设置。

例如:

java
WebClient client = WebClient.builder()
        .codecs(configurer -> ... )
        .build();
科特林
val webClient = WebClient.builder()
        .codecs { configurer -> ... }
        .build()

一旦构建, aWebClient是不可变的。但是,您可以克隆它并构建修改后的副本,如下所示:

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
科特林
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。最大内存大小

编解码器对在内存中缓冲数据有限制,以避免应用程序内存问题。默认情况下,这些设置为 256KB。如果这还不够,您将收到以下错误:

org.springframework.core.io.buffer.DataBufferLimitException:超过最大字节数限制

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

java
WebClient webClient = WebClient.builder()
        .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
        .build();
科特林
val webClient = WebClient.builder()
        .codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) }
        .build()

2.1.2. 反应堆网

要自定义 Reactor Netty 设置,请提供预配置HttpClient

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

WebClient webClient = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
科特林
val httpClient = HttpClient.create().secure { ... }

val webClient = WebClient.builder()
    .clientConnector(ReactorClientHttpConnector(httpClient))
    .build()
资源

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

如果服务器与进程同步,则通常不需要显式关闭。但是,如果服务器可以在进程内启动或停止(例如,部署为 WAR 的 Spring MVC 应用程序),您可以声明一个 Spring 管理的 bean 类型 ReactorResourceFactoryglobalResources=true默认)以确保 Reactor Netty 全局资源是在 Spring 关闭时ApplicationContext关闭,如下例所示:

java
@Bean
public ReactorResourceFactory reactorResourceFactory() {
    return new ReactorResourceFactory();
}
科特林
@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.
科特林
@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.
超时

要配置连接超时:

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();
科特林
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...
科特林
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...
科特林
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);
科特林
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。码头

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

java
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);

WebClient webClient = WebClient.builder()
        .clientConnector(new JettyClientHttpConnector(httpClient))
        .build();
科特林
val httpClient = HttpClient()
httpClient.cookieStore = ...

val webClient = WebClient.builder()
        .clientConnector(new JettyClientHttpConnector(httpClient))
        .build();

默认情况下,HttpClient创建自己的资源(ExecutorByteBufferPool、 ),这些资源在进程退出或被调用Scheduler之前保持活动状态。stop()

ApplicationContext您可以在 Jetty 客户端(和服务器)的多个实例之间共享资源,并通过声明类型为 Spring 的 Spring 管理的 bean 来确保在关闭 Spring 时关闭资源JettyResourceFactory,如以下示例所示:

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.
科特林
@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 HttpComponentsHttpClient设置:

java
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);

WebClient webClient = WebClient.builder().clientConnector(connector).build();
科特林
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);
科特林
val client = WebClient.create("https://example.org")

val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .toEntity<Person>().awaitSingle()

或者只得到身体:

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

Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono(Person.class);
科特林
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);
科特林
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);
科特林
val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError) { ... }
        .onStatus(HttpStatus::is5xxServerError) { ... }
        .awaitBody<Person>()

2.3. 交换

exchangeToMono()andexchangeToFlux()方法(或Kotlin 中的awaitExchange { }and 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);
            }
        });
科特林
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. 请求正文

请求正文可以从由ReactiveAdapterRegistry, likeMono或 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);
科特林
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);
科特林
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);
科特林
val person: Person = ...

client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(person)
        .retrieve()
        .awaitBody<Unit>()

2.4.1。表格数据

要发送表单数据,您可以提供一个MultiValueMap<String, String>作为正文。请注意,内容是application/x-www-form-urlencodedFormHttpMessageWriter. 下面的例子展示了如何使用MultiValueMap<String, String>

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

Mono<Void> result = client.post()
        .uri("/path", id)
        .bodyValue(formData)
        .retrieve()
        .bodyToMono(Void.class);
科特林
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);
科特林
import org.springframework.web.reactive.function.BodyInserters.*

client.post()
        .uri("/path", id)
        .body(fromFormData("k1", "v1").with("k2", "v2"))
        .retrieve()
        .awaitBody<Unit>()

2.4.2. 多部分数据

要发送多部分数据,您需要提供MultiValueMap<String, ?>其值是Object表示部分内容HttpEntity的实例或表示部分内容和标题的实例。MultipartBodyBuilder提供方便的 API 来准备多部分请求。下面的例子展示了如何创建一个MultiValueMap<String, ?>

java
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // Part from a server request

MultiValueMap<String, HttpEntity<?>> parts = builder.build();
科特林
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选择的序列化自动确定的,或者在 a 的情况下Resource,基于文件扩展名。如有必要,您可以通过重载的构建器方法MediaType之一为每个部分显式提供要使用的对象。part

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

java
MultipartBodyBuilder builder = ...;

Mono<Void> result = client.post()
        .uri("/path", id)
        .body(builder.build())
        .retrieve()
        .bodyToMono(Void.class);
科特林
val builder: MultipartBodyBuilder = ...

client.post()
        .uri("/path", id)
        .body(builder.build())
        .retrieve()
        .awaitBody<Unit>()

如果MultiValueMap包含至少一个非String值,也可以表示常规表单数据(即application/x-www-form-urlencoded),则无需将 设置Content-Typemultipart/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);
科特林
import org.springframework.web.reactive.function.BodyInserters.*

client.post()
        .uri("/path", id)
        .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
        .retrieve()
        .awaitBody<Unit>()

2.5. 过滤器

您可以通过 注册一个客户端过滤器(ExchangeFilterFunctionWebClient.Builder 来拦截和修改请求,如下例所示:

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

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

            return next.exchange(filtered);
        })
        .build();
科特林
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();
科特林
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();
科特林
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);
        }
    });
}
科特林
fun renewTokenFilter(): ExchangeFilterFunction? {
    return ExchangeFilterFunction { request: ClientRequest?, next: ExchangeFunction ->
        next.exchange(request!!).flatMap { response: ClientResponse ->
            if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
                [email protected] response.releaseBody()
                        .then(renewToken())
                        .flatMap { token: String? ->
                            val newRequest = ClientRequest.from(request).build()
                            next.exchange(newRequest)
                        }
            } else {
                [email protected] Mono.just(response)
            }
        }
    }
}

2.6. 属性

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

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

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

    }
科特林
val client = WebClient.builder()
        .filter { request, _ ->
            val usr = request.attributes()["myAttribute"];
            // ...
        }
        .build()

    client.get().uri("https://example.org/")
            .attribute("myAttribute", "...")
            .retrieve()
            .awaitBody<Unit>()

请注意,您可以在允许您将属性插入所有请求defaultRequest的级别全局配置回调 WebClient.Builder,这可以用于例如在 Spring MVC 应用程序中基于ThreadLocal数据填充请求属性。

2.7. 语境

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

Context为了适用于所有操作,需要在反应链的末端填充反应器。例如:

java
WebClient client = WebClient.builder()
        .filter((request, next) ->
                Mono.deferContextual(contextView -> {
                    String value = contextView.get("foo");
                    // ...
                }))
        .build();

client.get().uri("https://example.org/")
        .retrieve()
        .bodyToMono(String.class)
        .flatMap(body -> {
                // perform nested request (context propagates automatically)...
        })
        .contextWrite(context -> context.put("foo", ...));

2.8. 同步使用

WebClient可以通过在最后阻塞来以同步方式使用结果:

java
Person person = client.get().uri("/person/{id}", i).retrieve()
    .bodyToMono(Person.class)
    .block();

List<Person> persons = client.get().uri("/persons").retrieve()
    .bodyToFlux(Person.class)
    .collectList()
    .block();
科特林
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();
科特林
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())
    }

以上只是一个例子。还有许多其他模式和运算符可以组合一个反应式管道,该管道可以进行许多远程调用,可能是一些嵌套的、相互依赖的,直到最后都不会阻塞。

使用Fluxor Mono,您永远不必阻塞 Spring MVC 或 Spring WebFlux 控制器。只需从控制器方法返回生成的反应类型。同样的原理也适用于 Kotlin Coroutines 和 Spring WebFlux,只是Flow在你的控制器方法中使用暂停函数或返回。

2.9。测试

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

3. WebSockets

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

3.1。WebSocket 简介

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

WebSocket 交互以 HTTP 请求开始,该请求使用 HTTPUpgrade标头进行升级,或者在这种情况下,切换到 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连接。

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

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 不同,它不为消息的内容规定任何语义。这意味着除非客户端和服务器在消息语义上达成一致,否则无法路由或处理消息。

Sec-WebSocket-ProtocolWebSocket 客户端和服务器可以通过HTTP 握手请求的标头协商使用更高级别的消息传递协议(例如,STOMP) 。如果没有,他们需要提出自己的约定。

3.1.2。何时使用 WebSocket

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

例如,新闻、邮件和社交订阅源需要动态更新,但每隔几分钟更新一次可能完全没问题。另一方面,协作、游戏和金融应用程序需要更接近实时。

延迟本身并不是决定性因素。如果消息量相对较少(例如,监控网络故障),HTTP 流式传输或轮询可以提供有效的解决方案。正是低延迟、高频率和高容量的组合才成为使用 WebSocket 的最佳案例。

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

3.2. WebSocket API

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

3.2.1。服务器

要创建 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) {
        // ...
    }
}
科特林
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);
    }
}
科特林
@Configuration
class WebConfig {

    @Bean
    fun handlerMapping(): HandlerMapping {
        val map = mapOf("/path" to MyWebSocketHandler())
        val order = -1 // before annotated controllers

        return SimpleUrlHandlerMapping(map, order)
    }
}

如果使用WebFlux 配置,则无需进一步操作,否则如果不使用 WebFlux 配置,则需要声明 a WebSocketHandlerAdapter,如下所示:

java
@Configuration
class WebConfig {

    // ...

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}
科特林
@Configuration
class WebConfig {

    // ...

    @Bean
    fun handlerAdapter() =  WebSocketHandlerAdapter()
}

3.2.2.WebSocketHandler

take和 returnhandle方法指示会话的 应用程序处理何时完成。会话通过两个流处理,一个用于入站消息,一个用于出站消息。下表描述了处理流的两种方法:WebSocketHandlerWebSocketSessionMono<Void>

WebSocketSession方法 描述

Flux<WebSocketMessage> receive()

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

Mono<Void> send(Publisher<WebSocketMessage>)

获取传出消息的源,写入消息,并返回Mono<Void>在源完成和写入完成时完成的 a。

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

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

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

  • 在选定的点,通过 的close方法WebSocketSession

当入站和出站消息流组合在一起时,无需检查连接是否打开,因为 Reactive Streams 会发出结束活动的信号。入站流接收完成或错误信号,出站流接收取消信号。

处理程序的最基本实现是处理入站流的实现。下面的例子展示了这样一个实现:

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>接收完成时完成的 a。
科特林
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>接收完成时完成的 a。
对于嵌套的异步操作,您可能需要调用message.retain()使用池数据缓冲区的底层服务器(例如,Netty)。否则,数据缓冲区可能会在您有机会读取数据之前被释放。有关更多背景信息,请参阅 数据缓冲区和编解码器

以下实现结合了入站和出站流:

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>在我们继续接收时返回未完成的。
科特林
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>在任一流结束时完成的 a。
科特林
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>在任一流结束时完成的 a。

3.2.3。DataBuffer

DataBuffer是 WebFlux 中字节缓冲区的表示形式。参考资料的 Spring Core 部分在 Data Buffers and Codecs部分有更多内容。要理解的关键点是,在一些像 Netty 这样的服务器上,字节缓冲区是池化和引用计数的,并且必须在消耗时释放以避免内存泄漏。

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

3.2.4。握手

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

HandshakeWebSocketService公开一个sessionAttributePredicate属性,该属性允许设置 aPredicate<String>从 中提取属性WebSession并将它们插入到 的属性中WebSocketSession

3.2.5。服务器配置

RequestUpgradeStrategy为每个服务器公开特定于底层 WebSocket 服务器引擎的配置。使用 WebFlux Java 配置时,您可以自定义这些属性,如 WebFlux 配置的相应部分所示,否则如果不使用 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);
    }
}
科特林
@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并返回一个 CorsConfiguration带有允许的来源、标头和其他详细信息的。如果您不能这样做,您还可以在 上设置corsConfigurations属性SimpleUrlHandler以通过 URL 模式指定 CORS 设置。combine如果两者都指定,则使用on 方法将它们组合起来 CorsConfiguration

3.2.7。客户

Spring WebFluxWebSocketClient为 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());
科特林
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模块提供ServerHttpRequestServerHttpResponse和的模拟实现ServerWebExchange。有关模拟对象的讨论,请参阅Spring Web Reactive

WebTestClient构建在这些模拟请求和响应对象之上,为在没有 HTTP 服务器的情况下测试 WebFlux 应用程序提供支持。您也可以将WebTestClient用于端到端集成测试。

5.RSocket

本节介绍 Spring Framework 对 RSocket 协议的支持。

5.1。概述

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

  • Request-Response — 发送一条消息并接收一条返回。

  • Request-Stream — 发送一条消息并接收返回的消息流。

  • Channel — 双向发送消息流。

  • Fire-and-Forget — 发送单向消息。

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

这些是 RSocket 协议的主要特性和优点:

  • Reactive Streams跨网络边界的语义——对于流式请求,例如Request-StreamChannel,背压信号在请求者和响应者之间传播,允许请求者在源头减慢响应者的速度,从而减少对网络层拥塞控制的依赖,以及对缓冲的需求网络级别或任何级别。

  • 请求限制 - 此功能在可以从每一端发送的帧之后被命名为“租用”,LEASE以限制另一端在给定时间内允许的请求总数。租约会定期更新。

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

  • 大消息的分片和重组。

  • Keepalive(心跳)。

RSocket 有多种语言的实现。Java 库基于Project Reactor和用于传输的Reactor Netty构建。这意味着来自应用程序中 Reactive Streams Publishers 的信号通过 RSocket 在网络上透明地传播。

5.1.1。议定书

RSocket 的好处之一是它具有明确定义的在线行为和易于阅读的规范以及一些协议 扩展。因此,独立于语言实现和更高级别的框架 API 阅读规范是一个好主意。本节提供了一个简洁的概述来建立一些上下文。

连接

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

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

发出请求

一旦建立连接,双方可以通过帧REQUEST_RESPONSEREQUEST_STREAMREQUEST_CHANNEL或之一发起请求REQUEST_FNF。这些帧中的每一个都携带一条从请求者到响应者的消息。

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

当请求涉及消息流时,例如Request-StreamChannel,响应者必须尊重来自请求者的需求信号。需求以消息的数量表示。初始需求在REQUEST_STREAMREQUEST_CHANNEL框架中指定。随后的需求通过REQUEST_N帧发出信号。

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

消息格式

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

虽然所有消息都可以具有元数据,但通常元数据(例如路由)是每个请求的,因此仅包含在请求的第一条消息中,即带有帧 REQUEST_RESPONSEREQUEST_STREAMREQUEST_CHANNEL或中的一个REQUEST_FNF

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

5.1.2. Java 实现

RSocket的Java 实现建立在 Project Reactor之上。TCP 和 WebSocket 的传输建立在Reactor Netty之上。作为 Reactive Streams 库,Reactor 简化了实现协议的工作。对于应用程序,它非常适合使用声明式运算符FluxMono透明的背压支持。

RSocket Java 中的 API 是有意最小化和基本的。它专注于协议特性,并将应用程序编程模型(例如 RPC 代码生成与其他)作为更高级别的独立关注点。

主合约 io.rsocket.RSocket 对四种请求交互类型进行建模,分别Mono表示对单个消息的承诺、消息Flux流以及io.rsocket.Payload访问数据和元数据作为字节缓冲区的实际消息。RSocket合约是对称使用的。对于请求,应用程序被赋予RSocket执行请求的权限。为了响应,应用程序实现RSocket处理请求。

这并不意味着是一个彻底的介绍。在大多数情况下,Spring 应用程序不必直接使用它的 API。然而,独立于 Spring 查看或试验 RSocket 可能很重要。RSocket Java 存储库包含许多 演示其 API 和协议功能的示例应用程序。

5.1.3. 弹簧支持

spring-messaging模块包含以下内容:

  • RSocketRequesterio.rsocket.RSocket — 流畅的 API,通过 数据和元数据编码/解码 发出请求。

  • Annotated Responders  — @MessageMapping用于响应的带注释的处理程序方法。

spring-web模块包含RSocket 应用程序可能需要的 Jackson CBOR/JSON 和 ProtobufEncoder等实现。Decoder它还包含 PathPatternParser可以插入以进行有效路由匹配的内容。

Spring Boot 2.2 支持通过 TCP 或 WebSocket 建立 RSocket 服务器,包括在 WebFlux 服务器中通过 WebSocket 公开 RSocket 的选项。还有客户端支持和自动RSocketRequester.Builder配置RSocketStrategies。有关更多详细信息,请参阅 Spring Boot 参考中的 RSocket 部分

Spring Security 5.2 提供 RSocket 支持。

Spring Integration 5.2 提供了入站和出站网关来与 RSocket 客户端和服务器进行交互。有关详细信息,请参阅 Spring 集成参考手册。

Spring Cloud Gateway 支持 RSocket 连接。

5.2. RSocket请求者

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

5.2.1。客户请求者

在客户端获取一个RSocketRequester是连接到一个服务器,这涉及发送一个SETUP带有连接设置的 RSocket 帧。提供了一个构建器,可帮助为框架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);
科特林
val requester = RSocketRequester.builder().tcp("localhost", 7000)

URI url = URI.create("https://example.org:8080/rsocket");
val requester = RSocketRequester.builder().webSocket(url)

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

连接设置

RSocketRequester.Builder提供以下内容来自定义初始SETUP帧:

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

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

  • setupData(Object) — 要包含在SETUP.

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

  • setupMetadata(Object, MimeType) — 要包含在SETUP.

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

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

策略

RSocketRequester.Builder接受RSocketStrategies配置请求者。您需要使用它为数据和元数据值的(反)序列化提供编码器和解码器。默认情况下,仅注册来自spring-corefor Stringbyte[]和的基本编解码器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);
科特林
val strategies = RSocketStrategies.builder()
        .encoders { it.add(Jackson2CborEncoder()) }
        .decoders { it.add(Jackson2CborDecoder()) }
        .build()

val requester = RSocketRequester.builder()
        .rsocketStrategies(strategies)
        .tcp("localhost", 7000)

RSocketStrategies专为重复使用而设计。在某些情况下,例如同一应用程序中的客户端和服务器,最好在 Spring 配置中声明它。

客户响应者

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

您可以使用带注释的处理程序进行基于服务器上使用的相同基础架构的客户端响应,但以编程方式注册如下:

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 PathPatternRouteMatcher如果spring-web存在,则使用来进行有效的路由匹配。
2 @MessageMaping使用和/或@ConnectMapping方法从类创建响应者。
3 注册响应者。
科特林
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 PathPatternRouteMatcher如果spring-web存在,则使用来进行有效的路由匹配。
2 @MessageMaping使用和/或@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);
科特林
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)

对于上述情况,您可能还需要使用setHandlerPredicateinRSocketMessageHandler切换到检测客户端响应者的不同策略,例如基于自定义注释,例如@RSocketClientRespondervs default @Controller。这在具有客户端和服务器或同一应用程序中的多个客户端的情况下是必需的。

另请参阅Annotated Responders,了解有关编程模型的更多信息。

先进的

RSocketRequesterBuilder提供了一个回调来公开底层 io.rsocket.core.RSocketConnector的进一步配置选项,如keepalive间隔、会话恢复、拦截器等。您可以在该级别配置选项,如下所示:

java
RSocketRequester requester = RSocketRequester.builder()
    .rsocketConnector(connector -> {
        // ...
    })
    .tcp("localhost", 7000);
科特林
val requester = RSocketRequester.builder()
        .rsocketConnector {
            //...
        }
        .tcp("localhost", 7000)

5.2.2. 服务器请求者

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

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>
科特林
@ConnectMapping
suspend fun handle(requester: RSocketRequester) {
    GlobalScope.launch {
        requester.route("status").data("5").retrieveFlow<StatusReport>().collect { (1)
            // ...
        }
    }
    /// ... (2)
}
1 异步启动请求,独立于处理。
2 在挂起函数中进行处理。

5.2.3。要求

一旦你有一个客户端服务器请求者,你可以发出如下请求:

java
ViewBox viewBox = ... ;

Flux<AirportLocation> locations = requester.route("locate.radars.within") (1)
        .data(viewBox) (2)
        .retrieveFlux(AirportLocation.class); (3)
1 指定要包含在请求消息的元数据中的路由。
2 为请求消息提供数据。
3 声明预期的响应。
科特林
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,包括 Fluxand Mono,以及在 ReactiveAdapterRegistry. 对于产生相同类型值的多PublisherFlux,请考虑使用其中一种重载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);
科特林
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);
科特林
import org.springframework.messaging.rsocket.retrieveFlow

val requester: RSocketRequester = ...

val securityToken: String = ...
val viewBox: ViewBox = ...
val mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0")

val locations = requester.route("locate.radars.within")
        .metadata(securityToken, mimeType)
        .data(viewBox)
        .retrieveFlow<AirportLocation>()

用于Fire-and-Forget使用send()返回的方法Mono<Void>。请注意,Mono 仅表示消息已成功发送,而不表示已处理。

用于带有返回值Metadata-Push的方法。sendMetadata()Mono<Void>

5.3. 带注释的响应者

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

5.3.1。服务器响应程序

要在服务器端使用带注释的响应器,请添加RSocketMessageHandler到 Spring 配置中以使用和 方法检测@Controllerbean :@MessageMapping@ConnectMapping

java
@Configuration
static class ServerConfig {

    @Bean
    public RSocketMessageHandler rsocketMessageHandler() {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.routeMatcher(new PathPatternRouteMatcher());
        return handler;
    }
}
科特林
@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();
科特林
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 。

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

默认情况下SimpleRouteMatcher用于通过 匹配路由AntPathMatcher。我们建议插入PathPatternRouteMatcherfromspring-web以进行有效的路由匹配。RSocket 路由可以是分层的,但不是 URL 路径。两个路由匹配器都配置为使用“.” 默认情况下作为分隔符,并且没有与 HTTP URL 一样的 URL 解码。

RSocketMessageHandlerRSocketStrategies如果您需要在同一进程中的客户端和服务器之间共享配置,可以通过它进行配置:

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();
    }
}
科特林
@Configuration
class ServerConfig {

    @Bean
    fun rsocketMessageHandler() = RSocketMessageHandler().apply {
        rSocketStrategies = rsocketStrategies()
    }

    @Bean
    fun rsocketStrategies() = RSocketStrategies.builder()
            .encoders { it.add(Jackson2CborEncoder()) }
            .decoders { it.add(Jackson2CborDecoder()) }
            .routeMatcher(PathPatternRouteMatcher())
            .build()
}

5.3.2. 客户响应者

客户端带注释的响应者需要在 RSocketRequester.Builder. 有关详细信息,请参阅 客户端响应程序

5.3.3. @MessageMapping

一旦服务器客户端响应配置到位, @MessageMapping可以使用如下方法:

java
@Controller
public class RadarsController {

    @MessageMapping("locate.radars.within")
    public Flux<AirportLocation> radars(MapRequest request) {
        // ...
    }
}
科特林
@Controller
class RadarsController {

    @MessageMapping("locate.radars.within")
    fun radars(request: MapRequest): Flow<AirportLocation> {
        // ...
    }
}

上述@MessageMapping方法响应具有路由“locate.radars.within”的 Request-Stream 交互。它支持灵活的方法签名,可以选择使用以下方法参数:

方法论据 描述

@Payload

请求的有效负载。这可以是异步类型的具体值,例如 Monoor Flux

注意:注释的使用是可选的。不是简单类型且不是任何其他支持的参数的方法参数被假定为预期的有效负载。

RSocketRequester

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

@DestinationVariable

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

@Header

为提取注册的元数据值,如MetadataExtractor中所述。

@Headers Map<String, Object>

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

返回值应该是一个或多个要序列化为响应负载的对象。这可以是异步类型,例如Monoor Flux,具体值,或者是void或无值异步类型,例如Mono<Void>.

方法支持的 RSocket 交互类型@MessageMapping由输入(即@Payload参数)和输出的基数确定,其中基数的含义如下:

基数 描述

1

可以是显式值,也可以是单值异步类型,例如Mono<T>.

许多

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

0

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

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

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

输入基数 输出基数 交互类型

0, 1

0

即发即弃,请求-响应

0, 1

1

请求-响应

0, 1

许多

请求流

许多

0, 1, 许多

请求通道

5.3.4. @ConnectMapping

@ConnectMappingSETUP在 RSocket 连接开始时处理帧,以及任何后续元数据通过该METADATA_PUSH帧推送通知,即 metadataPush(Payload)io.rsocket.RSocket.

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

@ConnectMapping方法不能返回数据,必须使用void或 声明Mono<Void>为返回值。如果处理为新连接返回错误,则连接被拒绝。不得拖延处理以RSocketRequester向连接请求。有关详细信息,请参阅 服务器请求者

5.4. 元数据提取器

响应者必须解释元数据。 复合元数据允许独立格式化的元数据值(例如用于路由、安全、跟踪),每个都有自己的 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");
科特林
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);
    });
科特林
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)
}

通过配置MetadataExtractorRSocketStrategies,您可以让 RSocketStrategies.Builder使用配置的解码器创建提取器,并简单地使用回调来自定义注册,如下所示:

java
RSocketStrategies strategies = RSocketStrategies.builder()
    .metadataExtractorRegistry(registry -> {
        registry.metadataToExtract(fooMimeType, Foo.class, "foo");
        // ...
    })
    .build();
科特林
import org.springframework.messaging.rsocket.metadataToExtract

val strategies = RSocketStrategies.builder()
        .metadataExtractorRegistry { registry: MetadataExtractorRegistry ->
            registry.metadataToExtract<Foo>(fooMimeType, "foo")
            // ...
        }
        .build()

6. 反应式库

spring-webflux依赖reactor-core并在内部使用它来组合异步逻辑并提供 Reactive Streams 支持。通常,WebFlux API 返回FluxMono(因为它们在内部使用)并宽松地接受任何 Reactive Streams Publisher实现作为输入。Fluxvs的使用Mono很重要,因为它有助于表达基数——例如,是否需要单个或多个异步值,这对于做出决策至关重要(例如,在编码或解码 HTTP 消息时)。

For annotated controllers, WebFlux transparently adapts to the reactive library chosen by the application. This is done with the help of the ReactiveAdapterRegistry which provides pluggable support for reactive library and other asynchronous types. The registry has built-in support for RxJava 3, Kotlin coroutines and SmallRye Mutiny, but you can register other third-party adapters as well.

As of Spring Framework 5.3.11, support for RxJava 1 and 2 is deprecated, following RxJava’s own EOL advice and the upgrade recommendation towards RxJava 3.

对于函数式 API(例如Functional Endpoints、theWebClient等),适用于 WebFlux API 的一般规则—— FluxMono作为返回值和 Publisher作为输入的响应式流。当提供了一个Publisher,无论是自定义的还是来自另一个反应库的,它只能被视为具有未知语义(0..N)的流。但是,如果语义是已知的,则可以使用FluxMono.from(Publisher)代替传递 raw来包装它Publisher

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


1. see XML Configuration