对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

概述

为什么创建了 Spring WebFlux?spring-doc.cadn.net.cn

部分原因在于需要一个非阻塞的 Web 技术栈,以便使用少量线程处理并发,并以更少的硬件资源实现扩展。Servlet 的非阻塞 I/O 与 Servlet API 的其余部分相背离,因为其余部分的契约是同步的(FilterServlet)或阻塞的(getParametergetPart)。这促使我们创建一个新的通用 API,作为适用于任何非阻塞运行时环境的基础。这一点尤为重要,因为像 Netty 这样的服务器在异步、非阻塞领域已经非常成熟。spring-doc.cadn.net.cn

答案的另一部分是函数式编程。正如 Java 5 中引入注解创造了新的机会(例如带注解的 REST 控制器或单元测试),Java 8 中引入 Lambda 表达式也为 Java 中的函数式 API 创造了机会。这对于非阻塞应用程序和延续式(continuation-style)API(如 CompletableFutureReactiveX 所推广的)来说是一大利好,这类 API 允许以声明式的方式组合异步逻辑。在编程模型层面,Java 8 使 Spring WebFlux 能够在提供带注解的控制器的同时,也支持函数式 Web 端点。spring-doc.cadn.net.cn

定义“响应式”

我们提到了“非阻塞”和“函数式”,但“响应式”到底是什么意思呢?spring-doc.cadn.net.cn

“响应式”(reactive)一词指的是围绕对变化作出反应而构建的编程模型——例如网络组件对 I/O 事件作出反应,UI 控制器对鼠标事件作出反应,等等。 从这个意义上说,非阻塞就是响应式的,因为此时我们不再处于阻塞状态,而是转变为在操作完成或数据可用时对通知作出反应的模式。spring-doc.cadn.net.cn

Spring 团队还将“响应式”与另一种重要机制联系在一起,即非阻塞背压(non-blocking back pressure)。在同步的命令式代码中,阻塞调用作为一种天然的背压形式,强制调用方等待。而在非阻塞代码中,控制事件速率就变得至关重要,以防止快速的生产者压垮其接收端。spring-doc.cadn.net.cn

Reactive Streams 是一个 小型规范 (也在 Java 9 中被采纳), 它定义了具有背压机制的异步组件之间的交互方式。 例如,一个数据仓库(作为 发布者(Publisher)) 可以生成数据,而 HTTP 服务器(作为 订阅者(Subscriber)) 则可以将这些数据写入响应中。Reactive Streams 的主要目的是让 订阅者能够控制发布者生成数据的速度快慢。spring-doc.cadn.net.cn

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

响应式 API

Reactive Streams 在互操作性方面起着重要作用。它对库和基础设施组件很有价值,但作为应用程序 API 却不太实用,因为其层级过低。应用程序需要一个更高级、更丰富的函数式 API 来组合异步逻辑——类似于 Java 8 的 Stream API,但不仅限于集合。 这正是响应式库所扮演的角色。spring-doc.cadn.net.cn

Reactor 是 Spring WebFlux 的首选响应式库。它提供了 MonoFlux API 类型, 用于处理 0..1(Mono)和 0..N(Flux)的数据序列,并拥有丰富的操作符集合,这些操作符与 ReactiveX 的 操作符词汇表 保持一致。 Reactor 是一个响应式流(Reactive Streams)库,因此其所有操作符都支持非阻塞背压。 Reactor 专注于服务端 Java 开发,并与 Spring 紧密协作开发。spring-doc.cadn.net.cn

WebFlux 需要 Reactor 作为核心依赖,但通过 Reactive Streams 可与其他响应式库互操作。通常情况下,WebFlux API 接受一个普通的 Publisher 作为输入,在内部将其适配为 Reactor 类型并加以使用,然后返回一个 FluxMono 作为输出。因此,你可以传入任意的 Publisher 作为输入,并对输出应用各种操作,但在将输出用于其他响应式库时,你需要自行进行适配。在可行的情况下(例如带注解的控制器),WebFlux 能够透明地适配 RxJava 或其他响应式库的使用。更多详情请参见响应式库spring-doc.cadn.net.cn

除了响应式(Reactive)API 之外,WebFlux 还可以与 Kotlin 中的协程(Coroutines) API 一起使用,从而提供一种更具命令式风格的编程方式。 以下 Kotlin 代码示例将使用协程 API 提供。

编程模型

spring-web 模块包含作为 Spring WebFlux 基础的响应式基础架构, 包括 HTTP 抽象、针对受支持服务器的 Reactive Streams 适配器编解码器,以及核心的 WebHandler API,该 API 与 Servlet API 类似,但采用非阻塞契约。spring-doc.cadn.net.cn

在此基础上,Spring WebFlux 提供了两种编程模型供选择:spring-doc.cadn.net.cn

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

  • [webflux-fn]:基于 Lambda 表达式、轻量级且采用函数式编程模型。你可以将其视为一个小型库或一组工具,应用程序可利用它来路由和处理请求。与注解式控制器的主要区别在于:应用程序全程自主掌控请求的处理流程,而不是通过注解声明意图并等待框架回调。spring-doc.cadn.net.cn

适用性

Spring MVC 还是 WebFlux?spring-doc.cadn.net.cn

这是一个很自然的问题,但它却建立了一个不合理的二分法。实际上,两者协同工作,共同拓展了可用选项的范围。二者在设计上相互连贯、一致,可并行使用,并且来自任一方的反馈都能惠及双方。下图展示了二者之间的关系、它们的共同之处,以及各自独有的支持功能:spring-doc.cadn.net.cn

spring mvc and webflux venn

我们建议您考虑以下具体要点:spring-doc.cadn.net.cn

  • 如果你有一个运行良好的 Spring MVC 应用程序,就没有必要进行更改。 命令式编程是编写、理解和调试代码最简单的方式。 你可以最大限度地选择各种库,因为从历史上看,大多数库都是阻塞式的。spring-doc.cadn.net.cn

  • 如果你已经在寻找一个非阻塞的 Web 技术栈,Spring WebFlux 不仅能提供与其他同类框架相同的执行模型优势,还提供了多种服务器选择(Netty、Tomcat、Jetty、Undertow 和 Servlet 容器)、多种编程模型选择(注解式控制器和函数式 Web 端点),以及多种响应式库的选择(Reactor、RxJava 或其他)。spring-doc.cadn.net.cn

  • 如果你对一个轻量级、函数式的 Web 框架感兴趣,并希望在 Java 8 Lambda 表达式或 Kotlin 中使用,可以选择 Spring WebFlux 的函数式 Web 端点。对于需求较为简单的小型应用程序或微服务而言,这也是一个很好的选择,因为它们可以从更高的透明度和更强的控制能力中获益。spring-doc.cadn.net.cn

  • 在微服务架构中,你可以混合使用带有 Spring MVC 控制器、Spring WebFlux 控制器或 Spring WebFlux 函数式端点的应用程序。这两个框架都支持相同的基于注解的编程模型,这使得在选择合适工具完成特定任务的同时,更容易复用已有的知识。spring-doc.cadn.net.cn

  • 评估一个应用程序的简单方法是检查其依赖项。如果你需要使用阻塞式的持久化 API(如 JPA、JDBC)或网络 API,那么对于常见的架构而言,Spring MVC 至少是最好的选择。从技术上讲,使用 Reactor 或 RxJava 在单独的线程中执行阻塞调用是可行的,但这样做无法充分发挥非阻塞 Web 栈的优势。spring-doc.cadn.net.cn

  • 如果你有一个使用 Spring MVC 的应用程序,并且其中包含对远程服务的调用,可以尝试使用响应式的 WebClient。 你可以直接从 Spring MVC 控制器方法中返回响应式类型(Reactor、RxJava,或其他)。 每次调用的延迟越高,或者调用之间的相互依赖性越强,所带来的收益就越显著。Spring MVC 控制器也可以调用其他响应式组件。spring-doc.cadn.net.cn

  • 如果你拥有一个大型团队,请注意转向非阻塞、函数式和声明式编程所带来的陡峭学习曲线。一种无需完全切换即可开始实践的方法是使用响应式的 WebClient。除此之外,从小处着手,并衡量其带来的收益。我们预计,对于大量应用场景而言,这种转变并非必要。如果你不确定应该关注哪些收益,可以先了解非阻塞 I/O 的工作原理(例如单线程 Node.js 上的并发机制)及其影响。spring-doc.cadn.net.cn

服务器

Spring WebFlux 支持在 Tomcat、Jetty、Servlet 容器上运行,也支持在非 Servlet 运行环境(如 Netty 和 Undertow)上运行。所有服务器都被适配到一个底层的通用 API,以便更高层的编程模型可以在不同服务器之间通用。spring-doc.cadn.net.cn

Spring WebFlux 本身并不提供启动或停止服务器的内置支持。但是,通过 Spring 配置和组装应用程序、WebFlux 基础设施,并用几行代码运行它,是非常容易的。spring-doc.cadn.net.cn

Spring Boot 提供了一个 WebFlux Starter,可自动完成这些步骤。默认情况下,该 Starter 使用 Netty,但只需更改您的 Maven 或 Gradle 依赖项,即可轻松切换到 Tomcat、Jetty 或 Undertow。Spring Boot 默认选择 Netty,是因为它在异步、非阻塞领域应用更为广泛,并且允许客户端和服务器共享资源。spring-doc.cadn.net.cn

Tomcat 和 Jetty 均可用于 Spring MVC 和 WebFlux。但请注意,它们的使用方式存在很大差异。Spring MVC 依赖于 Servlet 阻塞式 I/O,并允许应用程序在需要时直接使用 Servlet API。而 Spring WebFlux 则依赖于 Servlet 非阻塞式 I/O,并通过一个底层适配器来使用 Servlet API,该 API 不会暴露出来供直接使用。spring-doc.cadn.net.cn

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

对于 Undertow,Spring WebFlux 直接使用 Undertow 的 API,而不通过 Servlet API。spring-doc.cadn.net.cn

性能

性能具有多种特性和含义。响应式和非阻塞通常并不会让应用程序运行得更快。在某些情况下可能会更快——例如,使用 WebClient 并行执行远程调用。然而,采用非阻塞方式实现功能需要更多的工作量,这可能会略微增加所需的处理时间。spring-doc.cadn.net.cn

响应式和非阻塞方式的主要预期优势在于,能够以少量且固定数量的线程以及更少的内存实现扩展。这使得应用程序在负载下更具弹性,因为它们能以更可预测的方式进行扩展。然而,要体现这些优势,系统中必须存在一定的延迟(包括较慢且不可预测的网络 I/O 混合情况)。正是在这种场景下,响应式技术栈开始展现出其优势,而且这种差异可能非常显著。spring-doc.cadn.net.cn

并发模型

Spring MVC 和 Spring WebFlux 都支持注解式控制器,但在并发模型以及对阻塞和线程的默认假设方面存在一个关键区别。spring-doc.cadn.net.cn

在 Spring MVC(以及一般的 Servlet 应用程序)中,假定应用程序可以阻塞当前线程(例如,用于远程调用)。因此,Servlet 容器使用一个较大的线程池,以吸收请求处理过程中可能出现的阻塞。spring-doc.cadn.net.cn

在 Spring WebFlux(以及一般的非阻塞服务器)中,假定应用程序不会发生阻塞。因此,非阻塞服务器使用一个小型、固定大小的线程池(事件循环工作线程)来处理请求。spring-doc.cadn.net.cn

“可扩展性”和“少量线程”听起来可能相互矛盾,但绝不阻塞当前线程(而是依赖回调)意味着你不需要额外的线程,因为根本不存在需要被吸收的阻塞调用。

调用阻塞式 API

如果你确实需要使用阻塞式库怎么办?Reactor 和 RxJava 都提供了 publishOn 操作符,以便在不同的线程上继续处理。这意味着存在一个简单的变通方案。但请记住,阻塞式 API 并不适合这种并发模型。spring-doc.cadn.net.cn

可变状态

在 Reactor 和 RxJava 中,您通过操作符来声明逻辑。在运行时,会形成一个响应式管道,数据在其中按顺序、分阶段进行处理。这样做的一个关键优势在于,它使应用程序无需再保护可变状态,因为该管道中的应用代码永远不会被并发调用。spring-doc.cadn.net.cn

线程模型

在运行 Spring WebFlux 的服务器上,你会看到哪些线程?spring-doc.cadn.net.cn

  • 在一个“原生”的 Spring WebFlux 服务器上(例如,不包含数据访问或其他可选依赖项),你可以预期服务器使用一个线程,另外还有若干线程用于请求处理(通常数量与 CPU 核心数相同)。然而,Servlet 容器可能会启动更多的线程(例如,Tomcat 默认启动 10 个线程),以同时支持 Servlet(阻塞式)I/O 和 Servlet 3.1(非阻塞式)I/O 的使用。spring-doc.cadn.net.cn

  • 响应式的 WebClient 以事件循环的方式运行。因此,你会看到与之相关的一小部分固定数量的处理线程(例如,使用 Reactor Netty 连接器时为 reactor-http-nio-)。然而,如果 Reactor Netty 同时用于客户端和服务器端,默认情况下两者会共享事件循环资源。spring-doc.cadn.net.cn

  • Reactor 和 RxJava 提供了称为调度器(schedulers)的线程池抽象,可与 publishOn 操作符配合使用,以将处理切换到不同的线程池。 这些调度器具有暗示特定并发策略的名称——例如,“parallel”(用于 CPU 密集型任务,使用数量有限的线程)或“elastic”(用于 I/O 密集型任务,使用大量线程)。如果你看到此类线程,说明某些代码正在使用特定的线程池 Scheduler 策略。spring-doc.cadn.net.cn

  • 数据访问库和其他第三方依赖项也可以创建并使用它们自己的线程。spring-doc.cadn.net.cn

配置

Spring 框架不提供启动和停止服务器的支持。要配置服务器的线程模型, 您需要使用特定于服务器的配置 API;或者,如果您使用 Spring Boot, 请查阅 Spring Boot 针对各服务器的配置选项。您可以直接配置WebClient。 对于所有其他库,请参阅它们各自的文档。spring-doc.cadn.net.cn