概述
为什么创建 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 端点。
定义“Reactive”
我们提到了“非阻塞”和“函数式”,但“响应式”是什么意思?
“响应式”一词指的是围绕对变化做出反应而构建的编程模型——网络组件对 I/O 事件做出反应,UI 控制器对鼠标事件做出反应,等等。从这个意义上说,非阻塞是响应式的,因为我们不再被阻塞,而是进入了对操作完成或数据可用时的通知做出反应的模式。
在 Spring 团队中,我们还将“反应式”与另一个重要机制联系在一起,那就是非阻塞背压。在同步的命令式代码中,阻塞调用作为一种自然的背压形式,迫使调用者等待。在非阻塞代码中,控制事件的速率变得很重要,以防止快速生产者压垮其目标。
常见问题:如果发布者无法减速怎么办?
Reactive Streams 的目的仅仅是建立机制和边界。如果发布者无法减速,它必须决定是缓冲、丢弃还是失败。
Reactive API
Reactive Streams 在互操作性方面起着重要作用。它对库和基础设施组件很有意义,但作为应用程序 API 则不太实用,因为它过于底层。应用程序需要一个更高级和更丰富的函数式 API 来组合异步逻辑——类似于 Java 8 的 Stream
API,但不仅限于集合。这就是反应式库所扮演的角色。
WebFlux 需要 Reactor 作为核心依赖,但通过 Reactive Streams 可以与其他响应式库互操作。一般来说,WebFlux API 接受一个普通的 Publisher
作为输入,内部将其适配为一个 Reactor 类型,使用该类型,并返回一个 Flux
或 Mono
作为输出。因此,你可以传递任何 Publisher
作为输入,并且可以对输出应用操作,但需要适配输出以便与其他响应式库一起使用。在可行的情况下(例如,注解控制器),WebFlux 会透明地适配 RxJava 或其他响应式库的使用。有关更多详细信息,请参见响应式库。
除了 Reactive API,WebFlux 还可以与 Kotlin 中的 协程 API 一起使用,这提供了一种更具命令式的编程风格。以下 Kotlin 代码示例将提供协程 API。
编程模型
spring-web
模块包含了 Spring WebFlux 的反应式基础,包括 HTTP 抽象、支持服务器的 Reactive Streams 适配器、编解码器,以及一个核心的 WebHandler API,它与 Servlet API 类似,但具有非阻塞契约。
在此基础上,Spring WebFlux 提供了两种编程模型的选择:
适用性
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 没有内置支持来启动或停止服务器。然而,从 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
策略。 -
数据访问库和其他第三方依赖项也可以创建和使用它们自己的线程。