概述

Spring WebFlux 是为何而创建的? 部分答案是为了需要一个非阻塞的 Web 堆栈,以用少量线程处理并发并以更少的硬件资源进行扩展。 Servlet 非阻塞 I/O 导致其偏离了 Servlet API 的其余部分,其中契约是同步的 (FilterServlet) 或阻塞的 (getParametergetPart)。 这是创建一个新的通用 API 的动机,该 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 中 被采用) 它定义了带有背压的异步组件之间的交互。 例如,一个数据仓库(作为 发布者) 可以生成数据,然后一个 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 一起使用,它提供了一种更命令式的编程风格。 以下 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 和 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 等非 Servlet 运行时。 所有服务器都适配到一个低级、 通用 API,以便在所有服务器上支持更高级的 编程模型

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

Spring Boot 有一个 WebFlux 启动器,可以自动化这些步骤。 默认情况下,该启动器使用 Netty,但通过更改 Maven 或 Gradle 依赖项,可以轻松切换到 Tomcat 或 Jetty。 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 将导致运行时问题。

性能

性能具有许多特性和含义。 响应式和非阻塞通常不会使应用程序运行得更快。 在某些情况下可以——例如,如果使用 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。 对于所有其他库,请参阅其各自的文档。