概述

Spring WebFlux 是如何创建的? 部分答案是为了满足对非阻塞 Web 堆栈的需求,以便用少量线程处理并发并以更少的硬件资源进行扩展。Servlet 的非阻塞 I/O 偏离了 Servlet API 的其余部分,其中契约是同步的 (Filter, Servlet) 或阻塞的 (getParameter, getPart)。这是创建新的通用 API 的动机,以作为任何非阻塞运行时的基础。这很重要,因为像 Netty 这样的服务器在异步、非阻塞领域已经很成熟。 答案的另一部分是函数式编程。正如 Java 5 中添加的注解创造了机会(例如带注解的 REST 控制器或单元测试)一样,Java 8 中添加的 lambda 表达式也为 Java 中的函数式 API 创造了机会。 这对非阻塞应用程序和延续式 API(由 CompletableFutureReactiveX 推广)来说是一个福音,它们允许声明性地组合异步逻辑。在编程模型层面,Java 8 使 Spring WebFlux 能够提供函数式 Web 端点以及带注解的控制器。

定义 “响应式”

我们已经提到了 “非阻塞” 和 “函数式”,但响应式意味着什么?

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

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

Reactive Streams 是一个 小型规范 (在 Java 9 中也 被采用), 它定义了带有背压的异步组件之间的交互。 例如,数据存储库(作为 Publisher) 可以生成数据,HTTP 服务器(作为 Subscriber) 然后可以将这些数据写入响应。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 一起使用,它提供了一种更命令式的编程风格。 以下 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 参数。

  • 函数式端点:基于 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 函数式端点。在这两个框架中支持相同的基于注解的编程模型,使得在选择合适的工具来完成合适的工作的同时,更容易重用知识。

  • 评估应用程序的一种简单方法是检查其依赖项。如果您有阻塞的持久化 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 策略。

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

配置

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