11.1使用Spring WebFlux
典型的基于Servlet的web框架,比如Spring MVC,本质上是阻塞和多线程的,每个连接使用一个线程。在处理请求时,将从线程池中提取一个工作线程来处理该请求。同时,请求线程被阻塞,直到工作线程通知它已完成为止。
因此,在请求量很大的情况下,阻塞web框架不能有效地扩展。慢工作线程中的延迟使情况更糟,因为工作线程池准备处理另一个请求所需的时间更长。在某些用例中,这种工作方式是完全可以接受的。事实上,这在很大程度上是大多数web应用程序十多年来的开发方式,但时代在变。
这些web应用程序伴随着HTTP API,已经从人们偶尔浏览网站成长为人们经常消费内容和使用应用程序。现在,所谓的 物联网(其中甚至没有人参与)产生的汽车、喷气发动机以及其他非传统的客户不断地通过web API交换数据。随着越来越多的客户使用web应用程序,扩展性比以往任何时候都更加重要。
相比之下,异步web框架实现用较少的线程达到更高的可扩展性,通常一个CPU一个线程。通过应用被称为event looping的技术(如图11.1所示),这些框架的每个线程都能够处理许多请求,使得每个连接的成本低 。
图11.1异步web框架通过应用event looping,使用较少的线程处理更多的请求

在一个event loop中,一切皆为事件,其中包括像是数据库和网络操作这种密集操作的请求与回调。当需要完成一个重要的操作时,event loop并行地为那个操作注册一个回调,然后它继续去处理其他事件。
当操作完成后,它会被event loop视为一个event,对于请求也是一样的操作。这样异步web框架就能够使用更少的线程应对繁重的请求,从而实现更好的扩展性,这样做的结果就是降低了线程管理的开销。
Spring 5已经基于Project Reactor推出了一个非阻塞异步web框架,以解决在web应用程序和API更大的可扩展性。让我们来看看Spring WebFlux —— 一个响应式web框架。
11.1.1 Spring WebFlux介绍
当Spring团队正在考虑如何添加一个响应式编程模型的网络层,很快就发现,如果不在Spring MVC做很大的改动,很明显这样做是很困难的。这将涉及到分支代码来决定是否响应式地处理请求。在本质上,其结果将是把两个web框架打包成一个,用if语句来分离响应式与非响应式。
最终决定创建一个单独的响应式web框架,这个框架尽可能的借鉴Spring MVC,而不是强行把响应式编程模型塞进Spring MVC中。Spring WebFlux就是这个框架了。图11.2展示了由Spring 5所定义的完整的web开发技术栈。
图11.2 Spring 5通过名为WebFlux的新web框架支持响应式式web应用程序,WebFlux是Spring MVC的兄弟,它们共享许多核心组件

在图11.2的左侧,可以看到SpringMVC技术栈,它是在Spring框架的2.5版中引入的。SpringMVC(在第2章和第6章中介绍)位于Java Servlet API之上,它需要一个Servlet容器(比如Tomcat)来执行。
相比之下,Spring WebFlux(在右侧)与Servlet API没有关系,因此它构建在一个响应式HTTP API之上,这个方式与使用Servlet API提供的相同的响应式功能类似。而且由于Spring WebFlux没有耦合到Servlet API,因此它不需要运行一个Servlet容器。相反,它可以在任何非阻塞web容器上运行,包括Netty、Undertow、Tomcat、Jetty或任何Servlet3.1或更高版本的容器。
图11.2最值得注意的是左上角的框,它表示了Spring MVC和Spring WebFlux之间常见的组件,主要是用于定义controller的注解。由于Spring MVC和Spring WebFlux共享相同的注解,Spring WebFlux在许多方面与Spring MVC没有区别。
右上角的框表示另一种编程模型,该模型使用函数式编程范式而不是使用注解来定义controller。我们将在第11.2节中详细讨论Spring的函数式web编程模型。
Spring MVC和Spring WebFlux之间最显著的区别就是添加到构建中的依赖项不同。在使用Spring WebFlux时,需要添加Spring Boot WebFlux starter依赖项,而不是标准的web starter(例如,spring-boot-starter-web)。在项目的pom.xml文件中,如下所示:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
注意:与大多数Spring Boot的starter依赖项一样,这个starter也可以通过选中initializer中的Reactive Web复选框添加到项目中。
使用WebFlux而不是Spring MVC的一个有趣的副作用是,WebFlux的默认嵌入式服务器是Netty而不是Tomcat。Netty是少数几个异步的事件驱动的服务器之一,它自然适合像Spring WebFlux这样的响应式web框架。
除了使用不同的starter依赖项之外,Spring WebFlux controller方法通常接受并返回响应式类型,比如Mono和Flux,而不是域类型和集合。Spring WebFlux控制器还可以处理RxJava类型,比如Observable、Single和Completable。
响应式Spring MVC?
尽管Spring WebFlux controller通常返回Mono和Flux,但这并不意味着Spring MVC在处理响应式类型时没有办法。如果你愿意,Spring MVC controller方法也可以返回Mono或Flux。
不同之处在于如何使用这些类型。Spring WebFlux是一个真正的响应式web框架,允许在event loop中处理请求,而Spring MVC是基于Servlet的,依赖多线程处理多个请求。
让我们通过重写一些Taco Cloud的API controller来让Spring WebFlux工作。
11.1.2编写响应式controller
你可能还记得,在第6章中,你为Taco Cloud的REST API创建了一些controller。这些controller具有处理请求的方法,这些方法根据域类型(如Order和Taco)或域类型的集合,处理输入和输出。提醒一下,请考虑你在第6章中写过的DesignTacoController中的以下片段:
@RestController
@RequestMapping(path="/design", produces="application/json")
@CrossOrigin(origins="*")
public class DesignTacoController {
...
@GetMapping("/recent")
public Iterable<Taco> recentTacos() {
PageRequest page = PageRequest.of(
0, 12, Sort.by("createdAt").descending());
return tacoRepo.findAll(page).getContent();
}
...
}
如前所述,recentTacos() controller处理 /design/recent
的HTTP GET请求,以返回最近创建的tacos的列表。更具体地说,它返回一个Iterable类型的Taco。这主要是因为这是从respository的findAll()方法返回的,或者更准确地说,是从findAll()返回的页面对象的getContent()方法返回的。
这很好,但是Iterable不是一个响应式的。你将不能对它应用任何响应式操作,也不能让框架利用它作为响应式类型在多个线程上分割任何工作。你想要的是recentTacos()返回一个Flux。
这里有一个简单但有点有限的选项,就是重写recentTacos()将Iterable转换为Flux。而且,当你使用它时,可以去掉分页代码,并用调用take()来替换它:
@GetMapping("/recent")
public Flux<Taco> recentTacos() {
return Flux.fromIterable(tacoRepo.findAll()).take(12);
}
使用Flux.fromIterable(),可以将Iterable转换为Flux。现在你正在使用一个Flux,可以使用take()操作将返回的Flux限制为最多12个Taco对象。不仅代码简单,它还处理一个响应式Flux,而不是一个简单的Iterable。
迄今为止,编写响应式代码是一个成功的举措。但是,如果repository提供了一个可以开始使用的Flux,那就更好了,这样就不需要进行转换。如果是这样的话,那么recentTacos()可以写成如下:
@GetMapping("/recent")
public Flux<Taco> recentTacos() {
return tacoRepo.findAll().take(12);
}
那就更好了!理想情况下,一个响应式cotroller将是一个端到端的响应式栈的顶端,包括controller、repository、database和任何可能位于两者之间的serviec。这种端到端的响应式栈如图11.3所示:
图11.3为了最大限度地发挥响应式web框架的优势,它应该是完整的端到端响应式堆栈的一部分

这样的端到端的栈要求repository被写入以返回一个Flux,而不是一个Iterable。在下一章中,我们将探讨如何编写响应式repostitory,但下面我们将看一看响应式TacoRepository可能是什么样子:
public interface TacoRepository extends ReactiveCrudRepository<Taco, Long> {
}
然而,在这一点上,最重要的是,除了使用Flux而不是Iterable以及如何获得Flux外,定义响应式WebFlux controller的编程模型与非响应式Spring MVC controller没有什么不同。两者都用@RestController和类级别的@RequestMapping进行了注解。它们都有请求处理函数,在方法级别用@GetMapping进行注解。真正的问题是处理程序方法返回什么类型。
另一个要做的重要观察是,尽管从repository中获得了一个Flux,但你可以在不调用subscribe()的情况下返回它。实际上,框架将为你调用subscribe()。这意味着当处理对 /design/recent
的请求时,recentTacos()方法将被调用,并在从数据库中获取数据之前返回!
返回单个值
作为另一个例子,请考虑DesignTacoController中的tacoById()方法,如第6章中所述:
@GetMapping("/{id}")
public Taco tacoById(@PathVariable("id") Long id) {
Optional<Taco> optTaco = tacoRepo.findById(id);
if (optTaco.isPresent()) {
return optTaco.get();
}
return null;
}
在这里,这个方法处理 /design/{id}
的GET请求并返回一个Taco对象。因为repository的findById()返回一个Optional,所以还必须编写一些笨拙的代码来处理这个问题。但是假设findById()返回Mono而不是Optional。在这种情况下,可以重写controller的tacoById(),如下所示:
@GetMapping("/{id}")
public Mono<Taco> tacoById(@PathVariable("id") Long id) {
return tacoRepo.findById(id);
}
哇!这就简单多了。然而,更重要的是,通过返回Mono而不是Taco,可以使Spring WebFlux以一种被动的方式处理响应。因此,你的API将更好地响应大的负载。
使用RxJava类型
值得指出的是,虽然在使用Spring WebFlux时,像Flux和Mono这样的Reactor类型是一个自然的选择,但是你也可以选择使用像Observable和Single这样的RxJava类型。例如,假设DesignTacoController和后端repository之间有一个service,它处理RxJava类型。在这种情况下,recentTacos()方法的编写方式如下:
@GetMapping("/recent")
public Observable<Taco> recentTacos() {
return tacoService.getRecentTacos();
}
类似地,可以编写tacoById()方法来处理RxJava的Single元素,而不是Mono:
@GetMapping("/{id}")
public Single<Taco> tacoById(@PathVariable("id") Long id) {
return tacoService.lookupTaco(id);
}
此外,Spring WebFlux controller方法还可以返回RxJava的Completable,这相当于Reactor中的Mono。WebFlux还可以返回一个Flowable,作为Observable或Reactor的Flux的替代。
响应式地处理输入
到目前为止,我们只关心控制器方法返回的响应式类型。但是使用Spring WebFlux,你还可以接受Mono或Flux作为处理程序方法的输入。请考虑DesignTacoController中postTaco()的原始实现:
@PostMapping(consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public Taco postTaco(@RequestBody Taco taco) {
return tacoRepo.save(taco);
}
正如最初编写的,postTaco()不仅返回一个简单的Taco对象,而且还接受一个绑定到请求主体内容的Taco对象。这意味着在请求有效负载完全解析并用于实例化Taco对象之前,无法调用postTaco()。这也意味着postTaco()在对repository的save()方法的阻塞调用,在返回之前无法返回。简言之,请求被阻塞了两次:当它进入postTaco()时,然后在postTaco()内部被再次阻塞。但是,通过对postTaco()应用一点响应式编码,可以使其成为一种完全无阻塞的请求处理方法:
@PostMapping(consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public Mono<Taco> postTaco(@RequestBody Mono<Taco> tacoMono) {
return tacoRepo.saveAll(tacoMono).next();
}
在这里,postTaco()接受Mono并调用repository的saveAll()方法,正如你将在下一章中看到的,该方法接受Reactive Streams Publisher的任何实现,包括Mono或Flux。saveAll()方法返回一个Flux,但是因为是从Mono开始的,所以Flux最多会发布一个Taco。因此,你可以调用next()来获取将从postTaco()返回的Mono。
通过接受Mono作为输入,可以立即调用该方法,而无需等待Taco从请求体被解析。由于repository也是被动的,它将接受一个Mono并立即返回一个Flux,从中调用next()并返回Mono。所有这些都是在处理请求之前完成的!
Spring WebFlux是Spring MVC的一个极好的替代品,它提供了使用与Spring MVC相同的开发模型编写响应式web应用程序的选项。不过,Spring 5还有另一个新的窍门。让我们看看如何使用Spring 5的新函数式编程风格创建响应式API。