Spring WebFlux框架

Spring WebFlux是Spring 5发布的响应式Web框架,从SpringBoot 2.x开始,默认采用Netty作为非阻塞I/O的Web服务器。

Spring WebFlux概述

Spring WebFlux基于Reactor框架,同时支持RxJava类库,构建响应式编程框架。查看WebFlux的Maven依赖,可以发现它依赖的项目工程包有Reactor、Spring、ReactiveX、RxJava等模块,使用WebFlux需要单独引用它的依赖包,WebFlux主要的包依赖关系如下图所示。


Spring WebFlux 详解_非阻塞


WebFlux的主要特征

● 采用Reactor响应式编程框架,同时提供对RxJava类库的支持。

● Spring WebFlux基于响应式流,可以建立异步、非阻塞、事件驱动的服务。

● Spring WebFlux和Reactor底层默认使用Netty作为Web服务器,使用线程收敛式方式处理I/O业务逻辑,同时支持异步Servlet 3.1容器(Tomcat、Jetty等)。

● Spring WebFlux同时支持响应式的WebSocket服务开发。

● 支持响应式HTTP客户端,可以用函数式方式异步非阻塞地发送HTTP请求。

WebFlux的主要模块

WebFlux的应用方式可以使用基于Spring Boot提供的开发模板,直接访问Spring Initializ网站,创建一个Maven或者Gradle项目,需要添加的依赖如下:


Spring WebFlux 详解_HTTP_02


在选择Spring Boot版本号时,需要选择2.0.0M2以后的版本才能正确加载WebFlux依赖包,下图是官方提供的Spring WebFlux与SpringMVC的架构对比。


Spring WebFlux 详解_HTTP_03


从图中我们可以基本了解WebFlux的主要模块。

● 底 层 是 Web HTTP 服 务 引 擎 , Spring MVC 框 架 基 于 传 统 的Servlet容器,WebFlux实现了Servlet 3.1+规范的容器引擎,Servlet 3.1规范中新增了对异步处理的支持,同时默认采用Netty的非Servlet引擎,采用I/O多路复用的异步非阻塞HTTP引擎。

● Spring MVC使用传统的Servlet API应用方式,而WebFlux基于响应式流框架,支持采用背压(Backpressure)方式的异步数据处理流标准。WebFlux默认继承了Reactor项目实现。

● Spring WebFlux的核心组件完成协调上下文及提供响应式编程支持的工作。

● Spring MVC主要使用注解的方式完成HTTP请求到方法的映射,WebFlux支持注解和函数式两种调用方式,通过函数式风格的API可以创建路由、Handler和Filter等服务组件。

WebFlux服务器开发

下面我们分别演示WebFlux提供的注解控制器模式和函数端点模式。介绍使用这两种编程模型实现的代码示例,以及它们不同的语法和差异。

注解控制器模式

注解控制器模式与Spring MVC一致,都基于与Spring Web模块相同的注释。Spring MVC和WebFlux控制器都支持反应式(Reactor、RxJava)数据返回类型,因此不容易将它们分开。一个显著的区别是,WebFlux还支持注解@RequestBody来处理事件响应。

首先,我们实现一个获取用户数据的Service,代码如下。

然后,定义UserController类,它是具体的Spring MVC控制器,

使用UserService获取数据,代码如下。


Spring WebFlux 详解_MVC_04



Spring WebFlux 详解_HTTP_05


然后,定义UserController类,它是具体的Spring MVC控制器,使用UserService获取数据,代码如下。


Spring WebFlux 详解_HTTP_06


函数端点模式

函数端点模式是基于Lambda的轻量级功能编程模型。可以将其视为小型库或应用程序,是可用于路由和处理请求的一组实用程序。它与注解控制器模式的巨大差异在于,应用程序负责从开始到结束的请求处理,并通过注解声明完成请求回调处理。

下面代码实现了TaskHandler(实现对UserService的访问)。


Spring WebFlux 详解_HTTP_07



Spring WebFlux 详解_HTTP_08


将URL路由和Handler函数绑定,代码如下。


Spring WebFlux 详解_HTTP_09


说明:WebFlux通过配置函数路由(RouterFunction)的方式来实 现 请 求 的 映 射 , 处 理 TaskHandler 的 方 法 的 返 回 类 型 是Mono<ServerResponse>。

下面我们看一下@FunctionInterface查看route的实现源码:


Spring WebFlux 详解_非阻塞_10


从 源 码 中 , 我 们 发 现 RouterFunction 返 回 一 个 <T extendsServerResponse>对象。在DefaultRouterFunction类中可以看到,在该类的route方法中可以判断请求的参数,如果值为空,则返回Empty,否则返回Mono<HandlerFunction<T>>的一个函数式接口,而这个函数就是Config中配置路由断言时指定的HandlerFunction。源码如下:


Spring WebFlux 详解_MVC_11



Spring WebFlux 详解_非阻塞_12


总之,由上面的源码分析可知,WebFlux底层虽然和传统的SpringWeb工作机制完全不同,但是WebFlux依然支持基于注解驱动的编程模型,区别在于WebFlux的并发模型和阻塞特性。函数端点模式是WebFlux通过配置函数路由的方式,实现请求到业务处理函数的映射。对于HTTP请求是如何从Web引擎映射到具体的实现方法的,下一节我们会继续介绍WebFlux的逻辑处理架构和HTTP请求的路由映射过程。

Spring WebFlux源码架构解析

与Spring MVC使用DispatcherServlet作为Servlet容器承上启下的重要管理组件类似,在Spring WebFlux框架中,DispatcherHandler有着异曲同工的作用。下面我们根据WebFlux源码讲解它的主要接口和模块的相互关系,WebFlux的工作流程如下图所示。


Spring WebFlux 详解_MVC_13


下面我们按照流程图的执行序列由上至下分别加以解释。

HTTP请求进入WebFlux处理引擎(Server Engine),这里默认使用Netty作为HTTP容器引擎。如果你想修改Servlet容器的服务引擎,则需要在pom.xml文件中添加相应的容器依赖Starter包,这里每个服务引擎都会有自己独立的响应适配器(Adapter)映射HTTP请求或响应到Server HttpRequest和Server HttpResponse组件。

HttpHandler 请 求 处 理 阶 段 主 要 将 输 入 的 请 求 ( ServerHttpRequest、Server HttpResponse,包括用户Session及相关信息)转换为Server WebExchange。

在WebFilterChain阶段,WebFlux会遍历之前注册到Spring容器的WebFilter对象,WebFilterChain负责执行所有WebFilter中的filter方法。对于WebFlux的Filter功能,一种方式是使用WebFilter在Spring MVC中的Filter接口,以接口的形式放回Mono<Void>;另一种方式是使用HandlerFilterFunction接口实现函数式的过滤调用。

如果所有WebFilter都通过执行并放行请求继续执行,那么WebFilterChain将调用WebHandler接口。

DispatcherHandler 实 现 了 WebHandler 接 口 , 同 时DispatcherHandler是WebFlux实现HTTP消息从框架层映射到对应业务逻辑方法的关键实现类。下面是WebFlux处理消息分发的关键源码:


Spring WebFlux 详解_MVC_14


从 上 面 的 代 码 可 知 , DispatcherHandler 的 主 要 流 程 是 遍 历HandlerMapping数据结构,并封装成数据流类Flux。它会触发对应的handler方法,执行相应的业务代码逻辑,而HandlerMapping在配置阶段 会 根 据 @Controller 、 @RequestMapping 、 @GetMapping 、@PostMapping注解注册对应的业务方法到HandlerMapping接口,这也是 WebFlux 兼 容 注 解 方 式 的 原 因 。 这 些 配 置 路 由 最 终 都 会 通 过getHandler()方法找到对应的处理类。

最后是
RequestMappingHandlerAdapter处理阶段,这个映射关系也适用于HandlerAdapter,我们将响应结果转变为数据流返回给handlerResult方法,并将结果转换成数据流序列返回。

WebClient开发

WebClient是从Spring WebFlux 5.0开始提供的一个非阻塞的、基于响应式编程范式的HTTP请求客户端工具。WebClient与传统的RestTemplate的主要区别在于基于函数式、响应式和流式的API,使用声明式的代码风格。同时WebClient依赖非阻塞式的编解码器来完成HTTP的请求和响应。

WebClient的构建

下 面 是 构 建 WebClient 的 一 个 简 单 实 例 。 首 先 通 过WebClient.create方法创建一个WebClient实例,然后通过get、post等方法选择适当的客户端调用方式,uri用来指定需要请求的路径,retrieve用来发起请求并获得响应,bodyToMono(String.class)用来指定请求结果需要处理为String、并包装为Reactor的Mono对象,代码如下所示:


Spring WebFlux 详解_非阻塞_15


除 了 通 过 create 方 法 构 建 WebClient , 也 可 以 通 过WebClient.builder方法创建WebClient.Builder对象。在对Builder对象进行一些配置后调用build方法创建WebClient对象。通过注册一个客 户 端 Filter ( ExchangeFilterFunction ) 实 现 拦 截 和 修 改HttpClient的头信息,同时执行baseURL和默认Cookie的构建,代码示例如下:


Spring WebFlux 详解_非阻塞_16


Builder自带的API构建WebClient的方法还有下面几种。

● uriBuilderFactory : 定 制 uriBuilderFactory 来 构 建baseURL。

● defaultHeader:每个HTTP请求默认使用的头信息。

● defaultCookie:每个HTTP请求默认使用的Cookie。

● defaultRequest:定制HTTP请求。

● filter:客户端可以构建filterBean实例来对WebClient的参数进行拦截过滤。

● exchangeStrategies:可以定制HTTP消息的发送接收策略。

● clientConnector:设置HTTP客户端。

WebClient的响应解析

WebClient使用retrieve()方法作为获取HTTP响应的最简单方法。它可以接受单个对象(

Mono),也可以接受数据流(Flux),同时可以判断返回的响应处理逻辑。

● 解析为Mono对象,代码示例如下。


Spring WebFlux 详解_HTTP_17


● 解析为Flux对象,代码示例如下。


Spring WebFlux 详解_MVC_18


● 通过onStatus()方法获取4xx/5xx的不同异常响应,代码示例如下。


Spring WebFlux 详解_非阻塞_19


WebClient使用exchange

相比retrieve方法,WebClient使用exchange方法可以对HTTP响应提 供 更 多 控 制 , 获 得 自 己 定 制 的 或 者 想 要 的 结 果 。 可 以 对clientResponse对象执行flatMap操作,代码如下。


Spring WebFlux 详解_HTTP_20


WebClient提交Body

假 设 WebClient 需 要 提 交 一 个 JSON 对 象 , 如 {“name” :“hello”,“id”:“123”},需要将这个对象传递给远端服务,WebClient会使用ReactiveAdapterRegitry来处理,将Body的异步编解码过程转换为JSON对象,代码示例如下。


Spring WebFlux 详解_MVC_21


当然也可以将编码后的JSON对象直接传递给WebClient,需要在HTTP头信息中指定ContentType为application/json,也可以加上charset编码。在默认情况下,WebClient将根据请求传递的对象进行解析,处理后自动选择ContentType。代码示例如下。


Spring WebFlux 详解_MVC_22


服务端推送事件

SSE服务端

服务端推送事件(Server-Sent Events,SSE)允许服务端不断地推送数据到客户端。相对于WebSocket而言,服务端推送事件只支持服务端到客户端的单向数据传递。SSE也是WebSocket的一个轻量级的替代方案,虽然功能较弱,但优势在于,SSE在已有的HTTP上可以使用简单易懂的文本格式来表示传输的数据。作为W3C的推荐规范,SSE在浏览器端的支持也比较广泛,除了IE,其他浏览器也都提供了支持。在IE上,也可以使用polyfill库来提供支持。对服务端来说,SSE是一个不断产生新数据的流,非常适合用响应式流来表示。在WebFlux中创建SSE 的 服 务 端 是 非 常 简 单 的 , 只 需 要 返 回 的 对 象 类 型 是Flux<ServerSentEvent>,就会自动按照SSE规范要求的格式来发送响应。

SseController是一个使用SSE的控制器。其中,randomNumbers方法 表 示 每 隔 一 秒 产 生 一 个 随 机 的 SSE 端 点 。 我 们 可 以 使 用ServerSentEvent.Builder类来创建ServerSentEvent对象。这里我们指定了事件的名称random,以及每个事件的标识符和数据。事件的标识符是一个递增的整数,而数据则是产生的随机数。下面的代码演示了服务推送事件。


Spring WebFlux 详解_HTTP_23


在测试SSE时,我们只需要使用curl来访问即可。下面的代码给出了调用curl http://localhost:8080/sse/randomNumbers的结果。


Spring WebFlux 详解_MVC_24


SSE客户端

WebClient还可以用同样的方式来访问SSE服务。这里我们访问的是在之前内容中创建的产生随机数的SSE服务。使用WebClient访问SSE服务在发送请求部分与访问Rest API是相同的,区别在于对HTTP响应的 处 理 。 由 于 SSE 服 务 的 响 应 是 一 个 消 息 流 , 我 们 需 要 使 用flatMapMany把Mono<ServerResponse>转换成Flux<ServerSentEvent>对象,这是通过BodyExtractors.toFlux方法完成的。

参数
newParameterizedTypeReference<ServerSentEvent<String>>(){}表明了响应式流中的内容是ServerSentEvent对象。由于SSE服务端会不断地发送消息,这里我们只是通过buffer方法来获取前10条消息并输出,代码如下所示。

Spring WebFlux的优势与局限

在传统的Java后台服务端开发中,我们使用Spring MVC框架的项目比较多,一个很自然的问题就是,对Spring MVC与Spring WebFlux技术栈的选择问题。开发者需要考虑从Spring MVC转型到SpringWebFlux框架的优势与局限。

Spring WebFlux与Spring MVC

下面是官方展示的Spring MVC与Spring WebFlux的框架对比图,可以看出两者在组件功能上的差异。


Spring WebFlux 详解_MVC_25


从上面的图中,我们可以看出两个框架在下面几方面的异同。

● 在编程模型上,Spring MVC偏向于命令式编程,其优点是简单、容易理解,并且对于开发者来说方便调试。而SpringWebFlux倾向于函数式编程模型。在调试和编程难度上相比Spring MVC,Spring WebFlux更大一些。然而如之前所说,函数式编程的优势是代码的可读性更强,更加强调不可变性,比命令式编程有更稳定的表现。

● 在线程模型上,Spring MVC主要受Servlet标准规范(3.x版本之前)的限制,所以主要使用同步式编程模型,通过线程的水平扩展来提升系统的吞吐和响应能力;Spring WebFlux使用事件触发机制的线程模型,在并发处理上可以使用少量的线程支撑高并发场景,收敛式的线程工作机制有利于充分利用物理资源,避免传统模式下线程阻塞等待的问题。

● 在上下游组件生态方面,Spring MVC有更强的优势,因为很多组件都使用了阻塞的交互方式,并且与Spring MVC框架都有很好的兼容性,所以生态方面有更好的适配和更高的成熟度。而Spring WebFlux因为采用异步非阻塞的响应式编程模型,所以目前在存储方面只有少数框架支持,主流的JDBC支持也还在探索当中。

● 共同点:Spring MVC和Spring WebFlux都可以使用注解式的开发方式,同时在Servlet 3.1异步规范下,Spring WebFlux也兼容主流的容器引擎,如Tomcat、Jetty等。同时,ReactiveClient作为异步的HttpClient也适用于Spring MVC。

Spring WebFlux的适用性

通过上面的特性对比,我们可以发现,虽然Spring WebFlux有诸多性能优势,但是,在业务的适用性和开发者的学习成本上还是有一定限制的。所以我们在架构迁移之前,需要做好准备,才能避免更多问题。下面是Spring官方给出的一些建议。

● 如果你现在使用Spring MVC框架运行,能够支持现有业务对性能的诉求,就尽量保持不变,Spring MVC有大量的类库可供使用,实现简单,易于理解。

● 如果你希望实现轻量级的函数式Web框架,那么可以考虑Spring WebFlux的函数式Web端点。

● 如果你依赖阻塞的持久化API,比如JPA或者JDBC等组件,那么就只能选择Spring MVC框架。目前Spring WebFlux对于非阻塞的JDBC实现,有一些早期的项目在探索,但是还没有成熟的技术方案。

● 在Spring MVC应用程序中进行远程调用,可以使用响应式的WebClient。Spring MVC也可以使用其他响应式组件。

● 对于大型应用程序要考虑到非阻塞方式实现业务功能的学习曲线。最简单的起步方式就是使用WebClient,完全切换到Spring WebFlux框架需要花费精力来熟悉相关的函数式编程API。

Spring WebFlux的局限

● 性能的局限

在使用Spring WebFlux过程中,我们很容易犯一个错误,就是误认为只要使用Spring WebFlux,我们的Web服务框架就能在性能上得到极大的提升。而本质上,性能是由很多不同的指标来度量的。

根 据 Spring 官 方 对 Spring WebFlux 框 架 的 性 能 分 析 , SpringWebFlux并不能使我们的程序跑得更快,在没有WebClient的情况下,请求的延迟时间可能比阻塞式Web框架更长。Spring WebFlux的真正优势是解决Web的吞吐问题,通过非阻塞的编程模型范式可以避免线程的阻塞等待,从而提升系统的整体服务容量。也就是说,SpringWebFlux通过少量的线程就可以处理和应付流量激增的请求,在牺牲小部分请求延迟的情况下,系统的整体资源利用率仍然可以保持稳定,而这要得益于响应式编程模型和非阻塞线程处理模型。

● 开发生态的局限

目前在Java企业开发中,Spring WebFlux是相对成熟的非阻塞式Web开发解决方案。虽然目前Spring生态中有对Redis和MongoDB的非阻塞框架支持,但是上述两种存储方案都基于内存的数据库,而SpringWebFlux访问关系数据库就成为一个绕不开的问题。我们知道JDBC连接池采用阻塞方式,如果使用阻塞的数据库访问方式,那么SpringWebFlux就会退化为传统的阻塞调用方式。

虽然目前有类库宣称已经实现了对JDBC的异步调用,但是并没有成熟的案例应用到生产或者实践中。如果你已经有了一个大型研发团队,还要用Spring WebFlux技术栈,就必须要权衡陡峭的学习曲线和实际的项目收益。如果想要在实际项目中应用异步非阻塞框架,一个切实可行的方法就是使用Spring WebFlux技术组件,如WebClient,通过渐进的技术模块逐步了解相关的技术生态。

● 学习曲线高的局限

Spring WebFlux还有一个局限,就是它的学习曲线相对命令式编程语言还是比较高的,响应式编程模型比函数式编程在语法上更难掌握。习惯于面向对象编程思维的开发者不容易适应响应式编程风格和以数据流驱动的思维模式。这给聚焦业务功能的开发者带来了较高的技术门槛。另外,响应式编程中常用的操作符,也比较难掌握,需要花费额外的工夫和精力才能完全掌握它的具体用法。

总结一下,响应式编程是高负载、高并发、大数据量场景下的应用解决方案,适用于在异步边界作为非阻塞模块交互的技术解决方案。如果你的应用对消息的实时性、高负载、用户量等方面没有太大的诉求,那么使用Spring MVC这样传统的编程框架就足够。所以,在进行技术选型或者编程模型选择时,首先要从业务的性质、用户规模和实际使用场景出发,还要考虑团队技术人员的学习能力和知识储备。选择Spring WebFlux作为Web服务器框架还需要从上述技术、业务、人员等因素来权衡利弊。