概述

为什么创建 Spring WebFlux?

答案的一部分是需要一个非阻塞式 Web 堆栈来处理少量线程的并发,并使用更少的硬件资源进行扩展。Servlet 非阻塞式 I/O 导致远离 Servlet API 的其余部分,其中契约是同步的(FilterServlet)或阻塞的(getParametergetPart)。这是创建新的通用 API 的动机,该 API 作为任何非阻塞式运行时的基础。这很重要,因为服务器(如 Netty)在异步、非阻塞式空间中已得到很好的建立。

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

定义“响应式”

我们提到了“非阻塞”和“函数式”,但“响应式”是什么意思呢?

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

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

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

常见问题:如果发布者无法减速怎么办?
Reactive Streams 的目的只是建立机制和边界。如果发布者无法减速,它必须决定是缓冲、丢弃还是失败。

响应式 API

Reactive Streams 在互操作性方面发挥着重要作用。它对库和基础设施组件很有用,但作为应用程序 API 却不太有用,因为它太底层了。应用程序需要一个更高级、更丰富的函数式 API 来组合异步逻辑——类似于 Java 8 的 Stream API,但不仅限于集合。这就是响应式库发挥作用的地方。

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

WebFlux 需要 Reactor 作为核心依赖项,但它可以通过 Reactive Streams 与其他响应式库互操作。一般来说,WebFlux API 接受一个普通的 Publisher 作为输入,将其内部转换为 Reactor 类型,使用它,并返回 FluxMono 作为输出。因此,您可以传递任何 Publisher 作为输入,并且可以对输出应用操作,但您需要调整输出以供其他响应式库使用。只要可行(例如,带注释的控制器),WebFlux 会透明地适应 RxJava 或其他响应式库的使用。有关更多详细信息,请参阅 响应式库

除了响应式 API 之外,WebFlux 还可以与 Kotlin 中的 协程 API 一起使用,该 API 提供了更命令式的编程风格。以下 Kotlin 代码示例将使用协程 API 提供。

编程模型

spring-web 模块包含 Spring WebFlux 的响应式基础,包括 HTTP 抽象、支持的服务器的 Reactive Streams 适配器编解码器 以及一个核心 WebHandler API,它类似于 Servlet API,但具有非阻塞契约。

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

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

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

适用性

Spring MVC 还是 WebFlux?

这是一个自然而然的问题,但它建立了一个不合理的二分法。实际上,两者协同工作,扩展了可用选项的范围。两者旨在彼此之间保持连续性和一致性,它们并排可用,并且来自各方的反馈都使双方受益。下图显示了这两者之间的关系,它们有哪些共同点,以及各自独特地支持什么

spring mvc and webflux venn

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

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

  • 如果您正在寻找一个非阻塞 Web 栈,Spring WebFlux 提供了与该领域其他框架相同的执行模型优势,并且还提供了服务器选择(Netty、Tomcat、Jetty、Undertow 和 Servlet 容器)、编程模型选择(带注释的控制器和函数式 Web 端点)以及响应式库选择(Reactor、RxJava 或其他)。

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

  • 在微服务架构中,您可以混合使用具有 Spring MVC 或 Spring WebFlux 控制器或 Spring WebFlux 函数式 Web 端点的应用程序。在两个框架中都支持相同的基于注释的编程模型,这使得在重用知识的同时选择合适的工具来完成合适的任务变得更加容易。

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

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

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

服务器

Spring WebFlux 支持 Tomcat、Jetty、Servlet 容器,以及 Netty 和 Undertow 等非 Servlet 运行时。所有服务器都适应于低级、通用 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 非阻塞 I/O,并在低级适配器后面使用 Servlet API。它不会公开供直接使用。

强烈建议不要在 WebFlux 应用程序的上下文中映射 Servlet 过滤器或直接操作 Servlet API。由于上述原因,在同一上下文中混合阻塞 I/O 和非阻塞 I/O 会导致运行时问题。

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

性能

性能具有多种特性和含义。响应式和非阻塞通常不会使应用程序运行得更快。在某些情况下它们可以 - 例如,如果使用WebClient并行运行远程调用。但是,以非阻塞方式执行操作需要更多工作,这可能会略微增加所需的处理时间。

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

并发模型

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 Netty 连接器时为 reactor-http-nio-)。但是,如果 Reactor Netty 用于客户端和服务器,则默认情况下两者共享事件循环资源。

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

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

配置

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