Spring 框架中包含的原始 Web 框架 Spring Web MVC 是专门为 Servlet API 和 Servlet 容器而设计的。后来在 5.0 版本中加入了 reactive 栈的 Web 框架 Spring WebFlux。它是完全非阻塞的,支持 Reactive Streams 背压,并在 Netty、Undertow 和 Servlet 容器等服务器上运行。
这两个Web框架都反映了其源模块的名称( spring-webmvc 和 spring-webflux)并在Spring框架中并存。每个模块都是可选的。应用程序可以使用一个或另一个模块,或者在某些情况下同时使用—例如,Spring MVC Controller 与响应式 WebClient。
一、 概览
为什么要创建 Spring WebFlux?
部分答案是需要一个非阻塞的Web栈来处理少量线程的并发,并以较少的硬件资源进行扩展。Servlet 的非阻塞I/O引出了Servlet API的其他部分,其中的约定是同步的(Filter,Servlet)或阻塞的(getParameter,getPart)。这是一个新的通用API的动机,作为跨越任何非阻塞运行时的基础。这一点很重要,因为服务器(如Netty)在异步、非阻塞领域已经很成熟了。
答案的另一部分是函数式编程。就像Java 5中增加的注解创造了机会(如注解的REST controller 或单元测试)一样,Java 8中增加的lambda表达式为Java中的函数式 API 创造了机会。这对非阻塞式应用和 continuation 式API(如 CompletableFuture 和 ReactiveX 所推广的)来说是个福音,它们允许异步逻辑的声明式组合。在编程模型层面,Java 8 使 Spring WebFlux 能够在注解的 controller 的同时提供函数式的 Web 端点。
二、定义 “Reactive” (响应式)
我们谈到了 "非阻塞" 和 "函数式",但响应式是什么意思?
术语 “reactive”(响应式), 指的是围绕对变化做出反应的编程模型—对I/O事件做出反应的网络组件,对鼠标事件做出反应的UI controller,以及其他。从这个意义上说,非阻塞就是响应式,因为,我们现在不是被阻塞,而是在操作完成或数据可用时对通知做出反(响)应。
还有一个重要的机制,我们Spring团队将其与 "响应式" 联系在一起,那就是非阻塞式背压。在同步的、命令式的代码中,阻塞式调用是一种自然的背压形式,迫使调用者等待。在非阻塞代码中,控制事件的速度变得很重要,这样快速的生产者就不会压倒其“消费者”。
Reactive Streams是一个 小型规范(也在Java 9中 采用),它定义了异步组件之间的互动,具有背压。例如,一个 data repository(作为 Publisher)可以产生数据,然后由一个HTTP服务器(作为 Subscriber)写入响应。Reactive Streams 的主要目的是让订阅者控制发布者产生数据的快慢。
常见的问题:如果 publisher 不能放慢速度怎么办? |
三、Reactive API
Reactive Streams 对互操作性起着重要作用。它对库和基础设施组件很有意义,但作为应用程序的API却不太有用,因为它太低级了。应用程序需要一个更高级别的、更丰富的、功能性的API来组成异步逻辑—类似于Java 8的 Stream API,但不只是用于集合。这就是响应式库所扮演的角色。
Reactor 是Spring WebFlux 的首选响应式库。它提供了 Mono 和 Flux API类型,通过与 ReactiveX 运算符词汇 相一致的丰富运算符集,对 0..1(Mono)和 0..N(Flux)的数据序列进行操作。Reactor是一个Reactive Streams库,因此,它的所有操作符都支持非阻塞的背压。Reactor非常关注服务器端的Java。它是与Spring紧密合作开发的。
WebFlux需要Reactor作为核心依赖,但它可以通过Reactive Streams与其他响应式库互操作。一般来说,WebFlux API接受一个普通的 Publisher 作为输入,在内部将其调整为Reactor类型,使用该类型,并返回 Flux 或 Mono 作为输出。因此,你可以传递任何 Publisher 作为输入,你可以在输出上应用操作,但你需要调整输出,以便与另一个响应式库一起使用。在可行的情况下(例如,注解 controller),WebFlux会透明地适应RxJava或其他响应式库的使用。更多细节请参见 响应式库。
除了Reactive APIs,WebFlux还可以与Kotlin中的 Coroutines APIs一起使用,它提供了一种更多的命令式编程风格。下面的Kotlin代码样本将提供Coroutines APIs。 |
四、编程模型
spring-web 模块包含了支撑Spring WebFlux的响应式基础,包括HTTP抽象、支持服务器的Reactive Streams 适配器、编解码器,以及与Servlet API相当但具有非阻塞约定的核心 WebHandler API。
在此基础上,Spring WebFlux提供了两种编程模型的选择:
- 注解式 Controller: 与Spring MVC一致,基于 spring-web 模块的相同注解。Spring MVC 和 WebFlux Controller 都支持响应式(Reactor和RxJava)返回类型,因此,要区分它们并不容易。一个明显的区别是,WebFlux也支持响应式的 @RequestBody 参数。
- 函数式端点: 基于Lambda的、轻量级的、函数式的编程模型。你可以把它看成是一个小型的库或一组实用程序,应用程序可以用它来路由和处理请求。和注解式 controller 的最大区别是,应用程序从头到尾负责处理请求,而不是通过注解声明意图并被回调。
五、 适用性
Spring MVC 还是 WebFlux?
这是一个很自然的问题,但它却设置了一个不健全的二分法。实际上,两者都是为了扩大可用选项的范围而共同工作。两者的设计都是为了彼此的连续性和一致性,它们可以并排使用,每一方的反馈都对双方有利。下图显示了这两者的关系,它们的共同点,以及各自支持的独特之处:
我们建议你考虑以下具体要点:
- 如果你有一个运行良好的Spring MVC应用程序,就没有必要改变。命令式编程是编写、理解和调试代码的最简单方式。你可以最大限度地选择库,因为从历史上看,大多数库都是阻塞的。
- 如果你已经在“选购”非阻塞式 web stack,Spring WebFlux 提供了与该领域其他公司相同的执行模式优势,还提供了服务器(Netty、Tomcat、Jetty、Undertow和Servlet容器)的选择、编程模式(注解式 controller 和函数式 web 端点)的选择,以及响应式库(Reactor、RxJava或其他)的选择。
- 如果你对用于Java 8 lambdas或Kotlin的轻量级功能性Web框架感兴趣,你可以使用Spring WebFlux 函数式 Web端点。对于需求不那么复杂的小型应用或微服务来说,这也是一个不错的选择,可以从更大的透明度和控制力中受益。
- 在微服务架构中,你可以将应用与 Spring MVC 或 Spring WebFlux controller 或与 Spring WebFlux 函数式端点混合在一起。在这两个框架中支持相同的基于注解的编程模型,可以更容易地重复使用知识,同时也可以为正确的工作选择正确的工具。
- 评估一个应用程序的简单方法是检查其依赖。如果你有阻塞的持久化API(JPA、JDBC)或网络API需要使用,Spring MVC 至少是普通架构的最佳选择。用 Reactor 和 RxJava 在单独的线程上执行阻塞调用在技术上是可行的,但你不会充分利用非阻塞的 Web stack。
- 如果你有一个调用远程服务的 Spring MVC 应用,可以试试响应式 WebClient。你可以直接从Spring MVC Controller 方法中返回响应式类型(Reactor、RxJava或 其他)。每次调用的延迟越大或调用之间的相互依赖性越大,好处就越明显。Spring MVC Controller 也可以调用其他响应式组件。
- 如果你有一个大的团队,请记住在转向非阻塞、函数式和声明式编程时的陡峭学习曲线。在没有完全转换的情况下,一个实用的方法是使用响应式的 WebClient。除此之外,从小处着手,并衡量其好处。我们预计,对于广泛的应用,这种转变是不必要的。如果你不确定要寻找什么好处,可以从了解非阻塞I/O的工作原理(例如,单线程Node.js的并发性)及其影响开始。
六、服务器
Spring WebFlux支持Tomcat、Jetty、Servlet容器,以及Netty和Undertow等非Servlet运行时。所有的服务器都适应于低级别的 通用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。它没有被暴露出来以供直接使用。
对于 Undertow,Spring WebFlux 直接使用 Undertow 的API,而不使用 Servlet API。
七、 性能
性能有很多特点和含义。响应式和非阻塞式通常不会使应用程序运行得更快。在某些情况下,它们可以(例如,如果使用 WebClient 来并行运行远程调用)。总的来说,用非阻塞的方式做事需要更多的工作,这可能会稍微增加所需的处理时间。
响应式和非阻塞式的关键预期好处是能够用少量的、固定的线程和较少的内存来扩展。这使得应用程序在负载下更有弹性,因为它们以一种更可预测的方式扩展。然而,为了观察这些好处,你需要有一些延迟(包括缓慢和不可预测的网络I/O的混合)。这就是 reactive stack 开始显示其优势的地方,其差异可能是巨大的。
八、 并发模型
Spring MVC和Spring WebFlux都支持注解 Controller ,但在并发模型以及阻塞和线程的默认假设方面有一个关键区别。
在Spring MVC(以及一般的servlet应用程序)中,假定应用程序可以阻塞当前线程,(例如,用于远程调用)。出于这个原因,servlet容器使用一个大的线程池来吸收请求处理过程中可能出现的阻塞。
在Spring WebFlux(以及一般的非阻塞服务器)中,假定应用程序不会阻塞。因此,非阻塞服务器使用一个小的、固定大小的线程池(event loop worke)来处理请求。
"扩展"和 "少量的线程" 听起来可能是矛盾的,但永远不阻塞当前线程(而依靠回调)意味着你不需要额外的线程,因为没有阻塞的调用需要吸收。 |
1、调用阻塞 API
如果你确实需要使用一个阻塞库呢?Reactor和RxJava都提供了 publishOn 操作符,可以在不同的线程上继续处理。这意味着有一个简单的救命稻草。然而,请记住,阻塞式API并不适合这种并发模型。
2、可变的状态
在Reactor和RxJava中,你通过操作符声明逻辑。在运行时,会形成一个响应式 pipeline,在这个 pipeline 中,数据被按顺序、分阶段地处理。这样做的一个主要好处是,它使应用程序不必保护易变的状态,因为该 pipeline 中的应用程序代码不会被并发调用。
3、线程模型
在使用Spring WebFlux运行的服务器上,你应该看到哪些线程?
- 在 "vanilla" Spring WebFlux 服务器上(例如,没有数据访问,也没有其他可选依赖),你可以期待服务器有一个线程,其他几个线程用于请求处理(通常与CPU核的数量一样多)。然而,Servlet容器开始时可能有更多的线程(例如Tomcat上的10个),以支持Servlet(阻塞)I/O和Servlet 3.1(非阻塞)I/O的使用。
- 响应式 WebClient 以事件循环(event loop)方式运行。所以你可以看到与之相关的少量固定的处理线程(例如,reactor-http-nio- 与 Reactor Netty connector)。然而,如果 Reactor Netty 同时用于客户端和服务器,两者默认共享事件循环资源。
- Reactor 和 RxJava 提供了线程池抽象,称为调度器(scheduler),与用于将处理切换到不同线程池的 publishOn 操作符一起使用。调度器的名字暗示了特定的并发策略—例如,“parallel”(用于有限数量线程的CPU绑定工作)或 “elastic”(用于有大量线程的I/O绑定工作)。如果你看到这样的线程,这意味着一些代码正在使用特定的线程池 Scheduler 策略。
- 数据访问库和其他第三方依赖也可以创建和使用它们自己的线程。
4、配置
Spring框架不提供对启动和停止 服务器 的支持。要配置服务器的线程模型,你需要使用服务器特定的配置API,或者,如果你使用Spring Boot,请检查每个服务器的Spring Boot配置选项。你可以直接 配置 WebClient。对于所有其他的库,请看它们各自的文档。