|
对于最新的稳定版本,请使用 Spring Framework 7.0.6! |
概述
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所普及的那样)允许声明式组合异步逻辑。在编程模型级别,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提供。 |
编程模型
The spring-web 模块包含了 Spring WebFlux 的响应式基础,包括 HTTP 抽象、受支持服务器的 Reactive Streams 适配器、编解码器,以及与 Servlet API 类似的但具有非阻塞契约的核心 WebHandler API。
在这个基础上,Spring WebFlux 提供了两种编程模型的选择:
-
注解控制器: 与Spring MVC一致,并基于来自
spring-web模块的相同注解。Spring MVC和WebFlux控制器都支持响应式(Reactor和RxJava)返回类型,因此很难区分它们。一个显著的区别是WebFlux还支持响应式@RequestBody参数。 -
[WebFlux函数式]:基于Lambda的、轻量级函数式编程模型。您可将其视为小型库或工具集,应用程序可用以路由和处理请求。与注解控制器的核心差异在于:应用程序需全程负责请求处理(而非通过注解声明意图并被动回调)。
适用性
Spring MVC 还是 WebFlux?
一个自然的问题,但却建立了一个不合理的二分法。实际上,两者共同作用以扩展可用选项的范围。这两者设计上注重连续性和一致性,它们并行存在,来自每一边的反馈都对两边有益。下图展示了两者之间的关系,它们的共同点,以及各自独特支持的内容:

我们建议您考虑以下具体要点:
-
如果你有一个运行良好的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 容器以及非 Servlet 运行时(如 Netty 和 Undertow)上运行。所有服务器都适配了底层 通用API,以便跨服务器支持更高层的 编程模型。
Spring WebFlux 没有内置的支持来启动或停止服务器。但是,从 Spring 配置和 组装 应用程序以及 WebFlux 基础设施 并用几行代码 运行它 是很容易的。
Spring Boot 有一个 WebFlux starter,可以自动化这些步骤。默认情况下,starter 使用 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, 该 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策略。 -
数据访问库和其他第三方依赖也可以创建和使用它们自己的线程。