1、SpringBoot 使用
1.1、Spring Core 中的响应式
Spring 生态系统的核心模块是Spring Core 模块。Spring 5.x 引入对响应式流和响应式库的原生支持,其中,响应式库包含RxJava 1/2 和Project Reactor 3。
1.1.1、响应式类型转换支持
为了支持响应式流规范所进行的最全面的改进之一是引入了 ReactiveAdapter 和 ReactiveAdapterRegistry 。ReactiveAdapter 类为响应式类型转换提供了两种基本方法,用于将任何类型转换为 Publisher<T> 并将其转换回 Object 。如以下源码所示:
java
org.springframework.core.ReactiveAdapter#toPublisher
java
org.springframework.core.ReactiveAdapter#fromPublisher
如,为了提供对RxJava 2 中的Maybe 响应式类型的转换,我们可以通过以下方式创建自己的 ReactiveAdapter:
java
/**
* 适配器的构造器,包含了异步类型或响应式类型与Reactive Streams Publisher之间的相互转换。
*
* @param descriptor 响应式类型描述符
* @param toPublisherFunction 转换到Publisher的适配器
* @param fromPublisherFunction 从Publisher转换的适配器
*/
public MayBeReactiveAdapter(
ReactiveTypeDescriptor descriptor,
Function<Object, Publisher<?>>
toPublisherFunction,
Function<Publisher<?>, Object>
fromPublisherFunction) {
super(descriptor, toPublisherFunction, fromPublisherFunction);
}
public MayBeReactiveAdapter() {
super(
ReactiveTypeDescriptor.singleOptionalValue(Maybe.class,Maybe::empty),
rawMaybe -> ((Maybe<?>) rawMaybe).toFlowable(),
publisher -> Flowable.fromPublisher(publisher).singleElement()
);
}
上面实例中,扩展了默认的 ReactiveAdapter 并提供了一个自定义实现。父构造函数的第一个参数是 ReactiveTypeDescriptor 实例的定义。ReactiveTypeDescriptor 提供了有关 ReactiveAdapter 中使用的响应式类型的信息。父构造函数需要定义转换函数,而该函数将原始对象( Maybe )转换为 Publisher 并将任何 Publisher 转换回 Maybe 。
为简化交互, ReactiveAdapterRegistry 使我们能将 ReactiveAdapter 的实例保存在一个位置并提供对它们的通用访问。如以下代码所示:
java
ReactiveAdapterRegistry.getSharedInstance().registerReactiveType(
ReactiveTypeDescriptor.singleOptionalValue(Maybe.class,Maybe::empty),
rawMaybe -> ((Maybe < ? > ) rawMaybe).toFlowable(),
publisher -> Flowable.fromPublisher(publisher).singleElement()
);
// 后续使用的时候直接通过共享实例访问
ReactiveAdapter maybeAdapter = ReactiveAdapterRegistry.getSharedInstance()
.getAdapter(Maybe.class);
如代码所示,ReactiveAdapterRegistry 表示针对不同响应式类型的ReactiveAdapter实例的公共池。同时,ReactiveAdapterRegistry 提供了一个单例实例,该实例既可以在框架内的许多地方使用,也可以在开发的应用程序中使用。
1.1.2、响应式IO
Spring Core 模块在 byte 缓冲区实例上引入了一个称为 DataBuffer 的抽象。之所以避免使用 java.nio.ByteBuffer ,主要是为了提供一个既可以支持不同字节缓冲区,又不需要在它们之间进行任何额外的转换的抽象。
例如,为了将 io.netty.buffer.ByteBuf 转换为 ByteBuffer ,必须访问所存储的字节,而这些字节可能需要从堆外空间被拉入到堆中。这可能破坏Netty 提供的高效内存使用和缓冲区回收(重用相同的字节缓冲区)。
Spring DataBuffer 提供特定实现的抽象,能以通用方式使用底层实现。DataBuffer 的 PooledDataBuffer 子接口,还启用了引用计数功能,并支持开箱即用的高效内存管理。
此外,Spring Core 的第五版引入了 DataBufferUtils 类,能以响应式流的形式与I/O 进行交互(与网络、资源、文件等交互)。例如,可以基于背压支持并通过以下响应式的方式读取文件内容:
java
Flux<DataBuffer> reactiveHamlet = DataBufferUtils.read(
new DefaultResourceLoader().getResource("Java入门到回家.txt"),
new DefaultDataBufferFactory(),
1024
);
DataBufferUtils.read 返回一个 DataBuffer 实例的 Flux 。因此,可以使用 Reactor 的所有功能读取文件内容。最后,与Spring Core 中响应式相关的最后一个意义重大且不可或缺的特性是响应式编解码器(reactive codecs)。响应式编解码器提供了一种将 DataBuffer 实例流 和 对象流 进行相互转换的简便方式。Encoder 和 Decoder 接口即用于此目的,并提供以下用于编码/解码数据流的API:
java
interface Encoder<T> {
Flux<DataBuffer> encode(Publisher<? extends T> inputStream,
DataBufferFactory bufferFactory,
ResolvableType elementType,
@Nullable MimeType mimeType,
@Nullable Map<String, Object> hints);
}
interface Decoder<T> {
Flux<T> decode(Publisher<DataBuffer> inputStream,
ResolvableType elementType,
@Nullable MimeType mimeType,
@Nullable Map<String, Object> hints);
Mono<T> decodeToMono(Publisher<DataBuffer> inputStream,
ResolvableType elementType,
@Nullable MimeType mimeType,
@Nullable Map < String, Object > hints);
}
两个接口都与响应式流中的 Publisher 一起运行,并能将 DataBuffer 实例流 编码/解码 为对象。它以非阻塞的方式,将序列化数据转换为java对象,将java对象序列化为序列化数据。
这种编码/解码数据的方式可以减少处理延迟,这是因为响应式流在本质上支持独立的元素处理,而不必等到最后一个字节才开始解码整个数据集。
1.2、响应式 Web
Spring Boot 2 引入了 WebFlux,支持高吞吐量、低延迟。Spring WebFlux建立在 响应式流适配器 之上,可以与 Netty 和 Undertow 以及基于 Servlet 3.1 的传统服务器等集成。Spring WebFlux 作为非阻塞的基础,将响应式流作为业务逻辑代码和服务器交互的中心抽象。
注意,Servlet API 3.1 的适配器提供了与Web MVC 适配器不同的纯异步和非阻塞集成。Spring Web MVC 模块也支持Servlet API 4.0,后者支持HTTP/2。
Spring WebFlux 将 Project Reactor 3 作为一等公民并广泛使用。响应式编程可以开箱即用,还可以在 Netty 上运行Web 应用程序。Spring WebFlux 模块提供内置的背压支持,可以确保 I/O 不会变得不堪重负。Spring WebFlux 的 WebClient 类,实现非阻塞的客户端交互。
旧Web MVC 模块还获得了对响应式流的一些支持。从框架的第五版开始,Servlet API 3.1 成为 Web MVC ++模块的基线++ 。意味着 Web MVC 现在++支持Servlet 规范提出的非阻塞I/O++ 。Web MVC 模块的设计在适当级别 的非阻塞 I/O 方面++没有太大变化++。
尽管如此,Servlet 3.0 的异步行为已经正确实现了一段时间。Spring Web MVC 为ResponseBodyEmitterReturnValueHandler 类提供了升级。
由于Publisher 类可能被视为无限的事件流,因此在不破坏 Web MVC 模块的整个基础结构的情况下,Emitter 处理程序是放置响应式处理逻辑的适当位置。为此,Web MVC 模块引入了ReactiveTypeHandler 类,它负责正确处理 Flux 和 Mono 等响应式类型。为了在客户端获得非阻塞行为,除了支持服务器端响应式类型的变更,还可以使用 WebFlux 模块所提供的 WebClient 。
Spring Boot 可以提供基于类路径中可用类的复杂环境管理行为。因此,通过提供WebMVC(spring-boot-starter-web)模块以及 WebFlux,我们可以从 WebFlux 模块获得 Web MVC 环境和非阻塞响应式 WebClient 。最后,当将这两个模块作为响应式管道进行比较时,得到的结构如下图所示:
在 Web MVC 或 WebFlux 这两种用法中,得到了几乎相同的基于响应式流的编程模型。这两个模块之间的显著差异之一是 Web MVC 需要在与源自旧模块设计的 Servlet API 集成时进行阻塞式写入或阻塞式读取。该缺陷导致响应式流内的相互作用模型退化,使其降级为普通的拉模型。
WebFlux 通信模型取决于网络吞吐量以及可定义其自身控制流的底层传输协议。总而言之,Spring 5 引入了一个强大的工具,用于使用响应式流规范和 Project Reactor 构建响应式非阻塞应用程序。此外,Spring Boot 支持强大的依赖管理和自动配置,可以保护我们免受依赖地狱的侵害。
1.3、响应式 Spring Data
Spring Data 主要提供对底层存储区域的同步阻塞访问。现在,Spring Data 框架提供了 ReactiveCrudRepository 接口,该接口暴露了 Project Reactor 的响应式类型。Spring Data 还提供了几个通过扩展 ReactiveCrudRepository 接口而与存储方法集成的模块。
- 基于 Spring Data Mongo 响应式模块的 **MongoDB:**与NoSQL 数据库之间的完全响应式非阻塞交互,同时也包含背压控制。
- 基于 Spring Data Cassandra 响应式模块的 **Cassandra:**与Cassandra 数据存储的异步非阻塞交互,支持基于TCP 流控制的背压。
- 基于 Spring Data Redis 响应式模块的 **Redis:**通过Lettuce Java 客户端实现的与 Redis 之间的响应式集成。
- 基于 Spring Data Couchbase 响应式模块的 **Couchbase:**通过基于 RxJava 的驱动程序实现的与 Couchbase 数据库之间的响应式Spring Data 集成。
此外,Spring Boot 提供了额外的启动器模块,可以与所选的存储方法实现平滑集成。除了NoSQL 数据库,Spring Data 还引入了 Spring Data JDBC ,与 JDBC 轻量级集成,快速提供响应式 JDBC 连接。其他 Spring 框架模块的大多数改进以 WebFlux 的响应式能力或响应式 Spring Data 模块为基础。
1.4、响应式 Spring Session
Spring 框架中与Spring Web 模块相关的另一个重要更新是 Spring Session 模块中的响应式支持。Spring Session 引入了 ReactiveSessionRepository,可以使用 Reactor 的 Mono 类型对存储的会话进行异步非阻塞访问。
除此之外,作为响应式 Spring Data 的会话存储,Spring Session 还提供与 Redis 的响应式集成。可以通过包含以下依赖项来实现分布式 WebSession:
XML
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
为了实现响应式 Redis WebSession 管理,必须将这3 个依赖项组合在一个地方。同时,Spring Boot 负责提供 bean 的精确组合并生成合适的自动配置,以便顺利地运行 Web 应用程序。
1.5、响应式 Spring Security
旧的Spring Security 使用 ThreadLocal 作为 SecurityContext 实例的存储方法。在单个Thread 内执行时,该技术很有效,在任何时候,都可以访问存储在ThreadLocal 中的SecurityContext。
但是,在执行异步通信时,该技术就会出现问题。这时,必须提供额外的工作来将 ThreadLocal 内容传输到另一个Thread,并为Thread 实例之间的每个切换实例执行此操作。
尽管Spring 框架通过使用一个额外的 ThreadLocal 扩展简化了 Threads 之间的 SecurityContext传输,但在基于 Project Reactor 或类似的响应式库应用响应式编程范例时,仍然会有问题。++新一代 Spring Security 采用了 Reactor 上下文功能++,以便在 Flux 或 Mono 流中传输安全上下文。通过这种方式,即使在运作着不同执行线程的复杂响应式流中,我们也可以安全地访问安全上下文。
1.6、响应式 Spring Cloud(重点🍕)
首先,响应式影响了分布式系统的入口点,即网关(gateway)。很长一段时间,唯一能够将应用程序作为网关运行的 Spring 模块是 Spring Cloud Netflix Zuul 模块。Netflix Zuul 基于使用阻塞同步请求路由的 Servlet API。使处理请求获得更好性能的唯一方法是调整底层服务器线程池。这种模型的伸缩性无法与响应式方法相比。
Spring Cloud 引入了新的 Spring Cloud Gateway 模块,该模块构建于 Spring WebFlux 之上,并在 Project Reactor 3 的支持下提供异步和非阻塞路由。除了新的网关模块,Spring Cloud Streams 还获得了Project Reactor 的支持,并且引入了更加细粒度的流模型。
为了简化响应式系统的开发,Spring Cloud 引入了一个名为 Spring Cloud Function 的新模块,该模块旨在为构建我们自己的函数即服务(function as a service ,FaaS)解决方案提供必要的组件。如果没有适当的附加基础设施,Spring Cloud Function 模块将无法应用在普通开发中。SpringCloud Data Flow 不仅提供了这种可能性,还包含了Spring Cloud Function 的部分功能。
1.7、响应式 Spring Test
Spring 生态系统提供了改进后的 Spring Test 和 Spring Boot Test 模块,它们扩展了一系列用于测试响应式 Spring 应用程序的附加功能。Spring Test 提供了一个 WebTestClient 来测试基于 WebFlux 的 Web 应用程序,同时,Spring Boot Test 使用普通的注解来处理测试套件的自动配置。
同时,为了测试响应式流的 Publisher,Project Reactor 提供了Reactor-Test 模块,它与 Spring Test 和 Spring Boot Test 模块相结合,可以为使用响应式Spring 实现的业务逻辑编写完整的验证套件。
1.8、响应式监控
基于 Project Reactor 和响应式 Spring 框架构建的面向生产的响应式系统应该暴露所有重要的运维指标。首先,Project Reactor 本身具有内置指标。它提供 Flux#metrics() 方法,可以跟踪响应式流中的不同事件。
Spring 框架生态系统提供了更新后的 Spring Boot Actuator 模块 ,该模块++支持应用程序监控和故障排除的主要指标++。新一代SpringActuator 提供与 WebFlux 的完全集成,并使用其异步、非阻塞编程模型,以便有效地暴露指标端点。
Spring Cloud Sleuth 模块提供了监控和跟踪应用程序的最终选项。该模块提供开箱即用的分布式跟踪,它的一个显著优点是支持 Project Reactor 的响应式编程,因此应用程序中的所有响应式工作流都可以被正确跟踪。
Spring 生态系统不仅改进了内核框架的响应性,还负责面向生产的功能,而且支持详细的应用程序监控(这种监控甚至包括这些功能的响应式解决方案)。
2、WebFlux 的应用
2.1、基于微服务的系统
WebFlux 的第一个应用是微服务系统。微服务系统最显著的特点是大量的I/O 通信。I/O 的存在,尤其是阻塞式I/O,会降低整体系统延迟和吞吐量。
2.1.1、微服务网关
1、Spring Cloud Gateway
Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,Spring Cloud Gateway 旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式。Spring Cloud Gateway 作为 Spring Cloud 生态系中的网关,目标是替代 Netflix ZUUL,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。
2、功能特征
- 基于 Spring Framework 5 , Project Reactor 和 Spring Boot 2.0
- 动态路由
- Predicates 和 Filters 作用于特定路由
- 集成 Hystrix 断路器
- 集成 Spring Cloud DiscoveryClient
- 易于编写的 Predicates 和 Filters
- 限流
- 路径重写
3、工作流程
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到实际服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前( pre )或之后( post )执行业务逻辑。
2.2、大文件上传
XML
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
src/main/java/com/lagou/webflux/demo/controller/FileController.java
java
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
@RestController
public class FileController {
@RequestMapping("/single")
public Mono < String > singleFile(@RequestPart("file") Mono < FilePart > file) {
return file.map(filepart -> {
Path tmpFile = null;
try {
// 创建临时文件
tmpFile = Files.createTempFile("file-",
filepart.filename());
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("文件路径:" + tmpFile.toAbsolutePath());
// 异步文件channel
AsynchronousFileChannel channel = null;
try {
// 打开指定文件写操作的channel
channel = AsynchronousFileChannel.open(tmpFile,
StandardOpenOption.WRITE);
} catch (IOException e) {
e.printStackTrace();
}
DataBufferUtils.write(filepart.content(), channel, 0)
.doOnNext(System.out::println)
.doOnComplete(() -> {
System.out.println("文件拷贝完成");
}).subscribe();
return tmpFile;
}).map(tmp -> tmp.toFile())
.flatMap(fileSingle -> file.map(FilePart::filename));
}
@RequestMapping(value = "/multi")
public Mono < List < String >> multiFiles(@RequestPart("file") Flux < FilePart >
filePartFlux) {
return filePartFlux.map(filePart -> {
Path tmpFile = null;
try {
tmpFile = Files.createTempFile("mfile-",
filePart.filename());
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(tmpFile.toAbsolutePath());
// 对每个filePart执行写文件的方法
filePart.transferTo(tmpFile.toFile());
// 返回Path对象
return tmpFile;
}).map(tfile -> tfile.toFile()) // 将每个Path对象映射为文件
.flatMap(fileSingle ->
filePartFlux.map(FilePart::filename)).collectList();
}
}
src/main/resources/static/index.html :
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
文件上传
</title>
</head>
<body>
<form action="/single" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="上传单个文件">
</form>
<hr>
<form action="/multi" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="file" name="file">
<input type="file" name="file">
<input type="file" name="file">
<input type="submit" value="上传多个文件">
</form>
</body>
</html>
2.3、处理客户端连接速度慢的系统
WebFlux 的第二个应用是构建系统,而这些系统的目标是在缓慢或不稳定网络连接条件下适用于移动设备客户端。要理解为什么WebFlux 在这个领域有用,就要回想一下在处理一个慢速连接时会发生什么。
问题在于,将数据从客户端传输到服务器可能花费大量时间,并且相应的响应也可能花费大量时间。在使用单连接单线程模型的情况下,已连接客户端数量越多,系统崩溃的可能性越大。例如,黑客能很容易的通过使用拒绝服务(Denial-of-Service,DoS)攻击使我们的服务器不可用。
相比之下,WebFlux 使我们能在不阻塞工作线程的情况下接受连接。这样,慢速连接不会导致任何问题。在等待传入请求体时,WebFlux 将继续接收其他连接而不会阻塞。响应式流抽象使我们能在需要时消费数据。这意味着服务器可以根据网络的就绪情况控制事件消费。
2.4、流系统或实时系统
WebFlux 的另一个有用的应用是实时流系统。要了解WebFlux 为什么能在这一点上提供帮助,就要回想实时流系统是什么。
首先,这些系统的特点是低延迟和高吞吐量。在流系统中,大多数数据是从服务器端传出的,因此客户端扮演消费者的角色。通常来自于客户端的事件少于来自于服务器端的事件。但是,在在线游戏等实时系统中,传入数据量等于传出数据量。
使用非阻塞通信可以实现低延迟和高吞吐量。正如前文所述,非阻塞异步通信可以实现高效的资源利用,而基于Netty 或类似框架的系统可以实现最高的吞吐量和最低的延迟。然而,这种响应式框架有其自身的缺点,即使用通道和回调的复杂交互模型。
尽管如此,响应式编程仍然可以巧妙地解决这两个问题。正如前面所说,响应式编程,尤其是响应式库(如Reactor 3)可以帮助我们构建一个异步的非阻塞流而只需要很少的开销。这些开销来自基础代码复杂性和可接受的学习曲线。这两种解决方案都包含在WebFlux中。使用Spring框架可以让我们轻松构建这样的系统。