概述
为什么创建 Spring WebFlux?
答案的一部分是需要一个非阻塞式 Web 栈来处理少量线程的并发,并使用更少的硬件资源进行扩展。Servlet 非阻塞 I/O 偏离了 Servlet API 的其余部分,其中契约是同步的(Filter
、Servlet
)或阻塞的(getParameter
、getPart
)。这是创建新的通用 API 的动机,以便作为任何非阻塞运行时基础的基础。这很重要,因为一些服务器(例如 Netty)在异步、非阻塞领域已经建立了良好的地位。
答案的另一部分是函数式编程。就像 Java 5 中添加注解创造了机会(例如带注解的 REST 控制器或单元测试)一样,Java 8 中添加 lambda 表达式为 Java 中的函数式 API 创造了机会。这对非阻塞应用程序和延续式 API(如 CompletableFuture
和 ReactiveX 推广的那样)非常有利,这些 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 选择的响应式库。它提供了 Mono
和 Flux
API 类型来处理 0..1(Mono
)和 0..N(Flux
)的数据序列,通过一系列与 ReactiveX 运算符词汇表 对齐的丰富运算符。Reactor 是一个 Reactive Streams 库,因此,它的所有运算符都支持非阻塞式背压。Reactor 非常注重服务器端的 Java。它与 Spring 密切合作开发。
WebFlux 需要 Reactor 作为核心依赖项,但它可以通过 Reactive Streams 与其他响应式库互操作。一般来说,WebFlux API 接受一个普通的 Publisher
作为输入,将其内部适配到 Reactor 类型,使用它,并返回 Flux
或 Mono
作为输出。因此,您可以传递任何 Publisher
作为输入,并且可以对输出应用操作,但是您需要适配输出以供另一个响应式库使用。只要可行(例如,带注解的控制器),WebFlux 会透明地适应使用 RxJava 或其他响应式库。有关更多详细信息,请参见 响应式库。
除了响应式 API 之外,WebFlux 还可以与 Kotlin 中的 协程 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 应用程序,则无需更改。命令式编程是编写、理解和调试代码最简单的方法。您可以最大程度地选择库,因为从历史上看,大多数库都是阻塞式的。
-
如果您已经在寻找非阻塞式 Web 栈,Spring WebFlux 提供了与该领域其他 Web 栈相同的执行模型优势,并且还提供了服务器选择(Netty、Tomcat、Jetty、Undertow 和 Servlet 容器)、编程模型选择(带注解的控制器和函数式 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 控制器也可以调用其他响应式组件。 -
如果您的团队规模较大,请记住向非阻塞、函数式和声明式编程的转变存在较陡峭的学习曲线。一个实用的入门方法是在不完全切换的情况下使用响应式的
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 的服务器上,您应该期望看到哪些线程?
-
在“普通”的 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
策略。 -
数据访问库和其他第三方依赖项也可以创建和使用自己的线程。