概述

Spring WebFlux是为什么而创建的?

部分原因是需要一个非阻塞的Web堆栈来处理并发,使用少量线程并且能够在较少的硬件资源下扩展。Servlet非阻塞I/O导致与Servlet API的其他部分背道而驰,其中合同是同步的(FilterServlet)或阻塞的(getParametergetPart)。这是创建一个新的通用API的动机,以作为任何非阻塞运行时的基础。这很重要,因为像Netty这样的服务器在异步、非阻塞空间中已经得到很好的建立。

答案的另一部分是函数式编程。就像Java 5中注解的添加创造了机会(例如注解的REST控制器或单元测试)一样,Java 8中lambda表达式的添加为Java中的函数式API创造了机会。这对于非阻塞应用程序和继续式API(如CompletableFutureReactiveX所推广的)是一个福音,它们允许声明式地组合异步逻辑。在编程模型级别,Java 8使Spring WebFlux能够在注解控制器旁边提供函数式Web端点。

定义“响应式”

我们提到了“非阻塞”和“函数式”,但响应式是什么意思?

“响应式”一词指的是围绕对变化做出反应构建的编程模型 - 网络组件对I/O事件做出反应,UI控制器对鼠标事件做出反应,等等。在这种意义上,非阻塞是响应式的,因为我们现在处于对通知做出反应的模式,当操作完成或数据变得可用时,我们不再被阻塞。

还有另一个与“响应式”相关的重要机制,那就是非阻塞背压。在同步的命令式代码中,阻塞调用作为一种自然形式的背压,迫使调用者等待。在非阻塞代码中,控制事件的速率变得很重要,以防止快速生产者压倒其目标。

Reactive Streams是一个小规范(也在Java 9中被采纳),它定义了具有背压的异步组件之间的交互。例如,一个数据存储库(充当发布者)可以生成数据,然后HTTP服务器(充当订阅者)可以将数据写入响应。Reactive Streams的主要目的是让订阅者控制发布者生成数据的速度。

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

响应式API

Reactive Streams在互操作性方面发挥着重要作用。对于库和基础设施组件来说,它很有用,但作为应用程序API,它不够高级。应用程序需要一个更高级和更丰富的函数式API来组合异步逻辑 - 类似于Java 8的Stream API,但不仅仅适用于集合。这就是响应式库发挥的作用。

Reactor是Spring WebFlux的响应式库选择。它提供了MonoFlux API类型,用于处理0..1(Mono)和0..N(Flux)的数据序列,通过与ReactiveX操作符词汇对齐的丰富操作符集。Reactor是一个Reactive Streams库,因此它的所有操作符都支持非阻塞背压。Reactor专注于服务器端Java。它是与Spring密切合作开发的。

WebFlux需要Reactor作为核心依赖项,但通过Reactive Streams与其他响应式库具有互操作性。一般规则是,WebFlux API接受一个普通的Publisher作为输入,内部将其适配为Reactor类型,使用它,并返回一个FluxMono作为输出。因此,您可以将任何Publisher作为输入,并且可以对输出应用操作,但需要将输出适配为另一个响应式库的使用。在可行的情况下(例如,注解控制器),WebFlux会自动适应使用RxJava或其他响应式库。有关更多详细信息,请参阅响应式库

除了响应式API外,WebFlux还可以与Kotlin中提供更具命令式编程风格的协程API一起使用。将提供以下带有协程API的Kotlin代码示例。

编程模型

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-fn]:基于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容器以及非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在低级别适配器后面。它不会直接暴露供直接使用。

强烈建议不要在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框架不提供启动和停止服务器的支持。要为服务器配置线程模型,您需要使用特定于服务器的配置API,或者如果使用Spring Boot,则检查每个服务器的Spring Boot配置选项。您可以直接配置WebClient。对于所有其他库,请参阅它们各自的文档。