Spring webflux
在目前的jdk 已经迭代22 虚拟线程(携程)来说 ,上下文的切换的压力得到优化,Spring webflux的热度也慢慢不温不火,但是其中的设计思想还是值得了解的
。
Spring WebFlux
1. 什么是 Spring WebFlux?
Spring WebFlux 是 Spring Framework 5 引入的非阻塞、响应式编程框架,它是基于异步 I/O 模型构建的。WebFlux 提供了一个事件驱动的响应式编程模型,可以处理大量并发请求而不会被 I/O 操作阻塞。WebFlux 可以运行在支持 Reactive Streams 的服务器(如 Netty)上,也可以在支持 Servlet 3.1+ 规范的容器(如 Tomcat、Jetty)上运行。
2. Spring WebFlux 与 Spring MVC 的主要区别
-
阻塞 vs. 非阻塞:
- Spring MVC 是阻塞 I/O,每个请求都需要一个线程来处理。如果线程等待 I/O 操作(如数据库查询),它会被阻塞。
- Spring WebFlux 使用非阻塞 I/O,线程不需要等待 I/O 操作完成,发出 I/O 请求后,线程可以释放去处理其他任务,当 I/O 操作完成时再回调处理结果。
-
同步 vs. 异步:
- Spring MVC 是同步的,每个请求会在一个线程上被处理到底,直到完成。
- Spring WebFlux 是异步的,任务被分为多个步骤,且不需要在同一个线程上执行。
这里可能一直对bio,nio,aio等问题困扰,这里就要说一下,底层使用到的是netty这种bio框架,所以需要先了解
BIO模型和多线程
即使在响应式编程和异步 I/O的模型中,本质上确实涉及多线程的概念,但它与传统的基于阻塞 I/O (BIO) 的多线程模型有显著区别。响应式编程和异步 I/O 通过一种更高效的方式管理线程和资源,从而避免了传统多线程模型的高开销和低效率。
BIO(阻塞 I/O)和多线程
在基于阻塞 I/O 的传统服务器模型(如 Spring MVC),每个请求会分配一个独立的线程。这个线程在等待 I/O 操作(例如数据库查询或外部 API 调用)时会进入阻塞状态,直到操作完成。在这种模型下,如果有多个请求同时到来,服务器需要创建或分配相应数量的线程。这种方式虽然简单直观,但存在一些问题:
- 高并发下资源浪费:大量线程会占用服务器的内存和 CPU 资源,特别是当线程在等待 I/O 时,资源仍然被锁定但没有被有效利用。
- 线程上下文切换开销:操作系统需要花费大量时间在不同的线程之间切换,这种线程上下文切换(context switching)开销在高并发环境下会变得非常明显,导致性能下降。
异步 I/O 和响应式模型的多线程
在 异步 I/O(如 Spring WebFlux 的实现)中,尽管仍然使用多线程,但其运作方式大大优化,避免了传统阻塞模型中的资源浪费和高开销。异步 I/O 主要通过 事件驱动 和 回调机制 来处理请求,从而达到高并发下的高效性能。
异步 I/O 的工作原理
-
事件循环线程模型:在异步 I/O 模型中,只有少量的线程(通常是 事件循环线程池)用于处理所有 I/O 操作。当请求到达时,系统不会为每个请求分配一个线程,而是将请求放入事件循环中,由少数线程来监控和处理。线程只负责启动 I/O 操作,并在 I/O 操作完成时,通过回调或事件触发进一步的处理。
-
非阻塞式 I/O:当系统发起一个 I/O 请求(如数据库查询或网络请求)时,线程不会等待该请求完成,而是将操作挂起,并继续处理其他请求或任务。当 I/O 操作完成时,操作系统会通知应用程序(通过回调或事件),并在事件循环中继续处理这个任务的剩余部分。
-
背压和资源控制:在响应式编程中,通常有背压(Backpressure)机制,它允许消费者(请求处理代码)根据自身的处理能力,来调节数据流的速率,防止服务器资源过载。
关键差异:多线程 vs 事件驱动
特性 | BIO(传统多线程模型) | 异步 I/O(响应式模型) |
---|---|---|
线程分配 | 每个请求一个线程 | 少量线程处理大量请求 |
I/O 操作 | 阻塞等待 | 非阻塞,异步处理 |
资源消耗 | 高,线程在等待 I/O 时浪费资源 | 低,线程被释放,继续处理其他任务 |
并发处理 | 线程数量受限,容易导致资源瓶颈 | 高效利用少量线程,适合高并发场景 |
线程上下文切换 | 高频上下文切换,开销大 | 减少上下文切换,开销小 |
响应性 | 处理时间依赖于线程是否可用和 I/O 阻塞 | 响应更快,避免线程被长时间占用 |
响应式编程中的多线程
尽管异步 I/O 和响应式编程模型更高效地使用了线程,但多线程仍然是不可避免的。只是:
-
线程数少:响应式系统通常使用更少的线程,比如基于 事件循环 的 Reactor Netty 或 Vert.x 通常只有固定数量的 I/O 线程,不会为每个请求创建新的线程。
-
线程不会被阻塞:线程不再是处理 I/O 操作的核心部分,而是负责调度和管理事件流。一旦发起 I/O 操作,线程会被释放出来,继续处理其他任务。
-
线程池管理:响应式系统通常会有不同的线程池来处理不同类型的任务。比如,I/O 操作可能由一个线程池处理,而 CPU 密集型任务由另一个线程池处理。这种分离使得系统在处理不同任务时更高效。
-
异步和回调:响应式编程强调 异步,操作通常通过回调链或响应式流(如
Mono
和Flux
)来完成,而不是通过线程阻塞来等待结果。这样可以充分利用服务器资源,即使在高并发情况下也能保持高效。
因此,异步 I/O 和响应式编程中的“多线程”不同于传统的多线程模型,它依赖于少量线程的高效利用和异步非阻塞的设计理念,能够在处理大量并发请求时保持高效的性能。
- 基于 Servlet vs. 基于 Reactive Streams:
- Spring MVC 依赖于 Servlet API,主要是传统的阻塞 I/O 模型。
- Spring WebFlux 依赖 Reactive Streams 规范,基于 Reactor 框架。
3. 异步 I/O 与非阻塞 I/O
要理解 Spring WebFlux 的核心异步和非阻塞编程模型,首先需要理解一些底层概念。
3.1 阻塞 I/O (Blocking I/O)
在传统的阻塞 I/O 模型中,一个线程发起一个 I/O 操作(如读取文件或数据库查询),它会被阻塞,即必须等待 I/O 操作完成,才能继续执行后续的代码逻辑。这个线程在等待期间无法处理其他任务。
缺点:
- 线程会因为等待 I/O 操作而浪费资源,系统的并发处理能力受限于线程池大小。
- 阻塞模型在高并发场景下不够高效,因为每个请求都会占用一个线程,而线程是稀缺资源。
3.2 非阻塞 I/O (Non-blocking I/O)
在非阻塞 I/O 模型中,线程发起 I/O 操作后不会被阻塞,而是立即被释放。I/O 操作在后台继续进行,线程可以继续执行其他任务。当 I/O 操作完成时,系统会通过回调机制或事件通知来告知线程操作已完成,线程再继续处理操作结果。
非阻塞 I/O 具体步骤:
- 发起 I/O 请求:线程发起 I/O 操作(如数据库查询),不会等待操作完成,而是继续处理其他请求。
- 释放线程:该线程不会因为 I/O 被占用,而是被释放回到线程池,可以处理其他任务。
- I/O 完成回调:当 I/O 操作完成时,操作系统或 I/O 子系统会发出事件通知,告知线程任务完成。线程会接收该通知并处理后续操作。
3.3 异步 I/O (Asynchronous I/O)
异步 I/O 通常与非阻塞 I/O 配合使用。异步 I/O 模型的特点是,I/O 操作不会在发起时阻塞线程,而是通过回调或事件机制来异步处理任务。
异步 I/O 和非阻塞 I/O 的区别:
- 非阻塞 I/O 强调的是线程不需要等待 I/O 操作的结果,可以继续执行其他任务。
- 异步 I/O 强调的是任务之间的非线性处理方式,任务的不同阶段通过回调机制来协同完成。
所以其实说简单一点就是,
传统的mvc服务器会每次收到一个请求就新建一个线程,来处理这个请求,而Spring webflux这种异步服务器类似线程池,web服务器会提前维护一批线程,用多路复用的方式,每次到达的请求就采用之前维护的线程进行处理请求,这样就不会每次一个线程就处理资源,
4. 核心机制: 线程池、事件驱动与多路复用
WebFlux 和其他基于异步 I/O 的服务器模型确实与 线程池 有很大相似之处,它们通过维护一组固定数量的线程(如 事件循环线程池)来处理大量的请求,而不为每个请求单独创建新线程。
核心理解:多路复用 + 线程池
-
事件循环和线程池:
- 事件循环模型:服务器通常会维护一小部分线程,这些线程负责处理所有的 I/O 事件。这个机制类似于操作系统中的 多路复用(I/O Multiplexing),如 Linux 下的
epoll
或select
,它们可以监听多个 I/O 操作,一旦有数据可用或操作完成,系统就会通知事件循环线程去处理。 - 线程池的概念:响应式模型的确会提前创建并维护一组有限的线程,并通过调度机制来管理这些线程处理所有的请求。这与传统的线程池概念非常相似,每个请求并不会占用一个独立的线程,而是由事件驱动机制和少量的线程协同处理。
- 事件循环模型:服务器通常会维护一小部分线程,这些线程负责处理所有的 I/O 事件。这个机制类似于操作系统中的 多路复用(I/O Multiplexing),如 Linux 下的
-
多路复用的工作原理:
- 当一个请求到达时,服务器并不会立刻创建一个新线程来处理该请求,而是将请求注册到事件循环中。如果该请求涉及 I/O 操作(如文件读写、数据库查询等),事件循环会在 I/O 完成时通知相应的线程去处理这个任务,而这个线程在等待期间可以继续处理其他请求。
- 通过这种 多路复用 的方式,服务器不需要每个请求都占用一个独立的线程,从而减少了线程的上下文切换和阻塞,极大地提升了系统的并发处理能力。
为什么不会造成性能瓶颈?
虽然线程池和事件循环的线程是固定数量的,但性能不会受到太大影响,原因在于以下几个关键点:
-
非阻塞 I/O:
- 在线程池模型中,每个线程并不会因为 I/O 操作而被阻塞。传统的阻塞模型会让线程等待 I/O 操作(如网络请求或数据库查询)完成,但在非阻塞 I/O 模型中,线程在发起 I/O 请求后会立即被释放,去处理其他任务。
- 只有在 I/O 操作完成时,系统才会通知线程去处理后续逻辑。因此,即便线程数量是固定的,它们能在同一时间处理大量并发请求。
-
少量线程处理大量任务:
- 因为线程在异步模型中不需要等待 I/O 操作的结果,所以一个线程可以同时处理多个请求的不同阶段。假设有 1000 个并发请求,其中大多数需要等待数据库或外部服务返回数据,那么这 1000 个请求并不会需要 1000 个线程来处理,而可能只需要 10-20 个线程。
- 通过事件驱动模型,线程只在需要处理业务逻辑时才被调用,避免了资源浪费。
-
背压(Backpressure)机制:
- 响应式系统有一套背压机制来防止请求过载。假设请求量瞬间激增,系统可以通过背压控制数据流的速度,保证系统不会因为请求过多而崩溃。线程池中空闲的线程会按需分配,避免了线程过载或过多的并发请求导致性能下降。
-
减少线程上下文切换:
- 传统的多线程模型中,频繁的线程创建和销毁,以及大量的上下文切换,会对系统性能造成较大影响。非阻塞模型通过少量线程去处理任务,减少了线程切换的频率,这样 CPU 资源更多用于处理实际业务逻辑,而不是在线程调度和切换上浪费。
响应时间不会增加的原因:
-
高效的线程利用率:
因为线程不被 I/O 操作阻塞,所以即使线程数量有限,每个线程的利用率会非常高。当一个线程等待 I/O 操作完成时,它可以去处理其他请求的业务逻辑。这样可以提高线程的并发处理能力,减少系统在高并发情况下的响应延迟。 -
异步回调机制:
通过异步编程模型,I/O 操作的完成不再依赖于线程阻塞,而是通过回调机制通知相关线程继续处理剩余任务。这种模式下,系统可以快速处理请求的部分业务逻辑,并通过异步方式等待 I/O 操作完成。最终的响应时间是由整体的异步任务完成时间决定的,而不是由单个线程的阻塞状态决定。 -
事件驱动响应更快:
非阻塞 I/O 模型通过事件驱动方式处理请求,因此在高并发的情况下,服务器能迅速对请求做出反应,而不会因为线程资源耗尽导致响应时间增加。通过优化的 I/O 多路复用机制,系统可以快速侦听和处理大量请求,而不会因为线程数量有限而延迟响应。
总结
WebFlux 的确通过 线程池 和 多路复用 的方式来处理请求,避免了传统每个请求都创建新线程的做法。虽然维护的线程数量是固定的,但由于采用了 非阻塞 和 异步事件驱动 的机制,线程并不会因为等待 I/O 而被阻塞,从而能够同时处理多个请求。这样一来,即使线程数量有限,系统仍然能够高效处理高并发请求,保持良好的响应时间。
在理解 WebFlux 时,最核心的点是事件驱动模型和I/O 多路复用,以及它如何通过少量线程处理大量并发请求。
4.3 线程池
尽管 WebFlux 使用的是非阻塞 I/O,但它仍然依赖一个小型线程池来处理业务逻辑。每个线程不需要等待 I/O 操作的完成,可以并发地处理多个请求,从而减少线程上下文切换的开销。
5. Spring WebFlux 编程模型
5.1 核心类:Mono 和 Flux
在 WebFlux 中,处理请求的结果通常以两种响应类型表示:Mono
和 Flux
。
webflux构建的异步服务器中,返回的响应都是通过flux和mono包裹的异步序列
- Mono:表示 0 或 1 个元素的异步序列,常用于处理单一结果。
- Flux:表示 0 到 N 个元素的异步序列,常用于处理多个结果。
示例:
/*
包裹像响应结构题体
*/
@GetMapping
public Mono<String> getHelloMessage() {
return Mono.just("Hello, WebFlux!");
}
@GetMapping("2")
public Flux<String> getNames() {
return Flux.just("John", "Jane", "Jack");
}
数据订阅
result.subscribe(s -> log.info("get2 result: {}", s));
just 是立即包裹数据在异步响应中,类似传统mvc的响应体 ,线程会阻塞在这里等待包装结果
如果处理结果比较耗时也可以采用异步包装
// 阻塞5秒钟 模拟任务耗时
private String createStr() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
}
return "some string";
}
// 普通的SpringMVC方法
@GetMapping("/1")
private String get1() {
log.info("get1 start");
String result = createStr();
log.info("get1 end.");
return result;
}
// WebFlux(返回的是Mono)
@GetMapping("/2")
private Mono<String> get2() {
log.info("get2 start");
Mono<String> result = Mono.fromSupplier(() -> createStr());
log.info("get2 end.");
return result;
}
代码到这里,很多时候,会把nio和多线程搞混了,这里肯定会想,线程执行这个方法,而此时不会阻塞等待结果执行完成,所以会认为当这个方法处理时间过长,返回响应的结果就为null
这样其实是很并发情况理解混乱了
由于nio的特性当代码块执行到这个createStr()时候,并不会阻塞,而是继续执行下面的代码,如果 createStr() 方法耗时很长,它的执行会被延迟到消费者(客户端)订阅 Mono 时。因此在 get2() 方法返回时,result 变量并不会导致返回 null。实际上是返回了一个包装了 createStr() 的 Mono 对象,而不是 createStr() 的结果,
Mono.fromSupplier(() -> createStr()) 会创建一个 Mono 实例,并将 createStr() 方法包装成一个供应者(supplier)。
此时,createStr() 不会被立即执行,只有在订阅 Mono 时,它才会执行。而在webflux构建的服务器中,用户的访问请求就类似一个隐式的订阅,mono包裹这个行为给客户端,客户端获取到这个行为,然后等当有客户端请求来订阅它时,createStr() 方法才会被调用。
如果 createStr() 在调用时耗时过长,客户端会在等待结果,但此时不会返回 null。相反,Spring WebFlux 会保持连接,直到 createStr() 完成,并通过 Mono 返回结果。
并且支持传统的mvc方式注册Controller·路由
@RestController
@RequestMapping("/api")
public class BasicController {
@GetMapping("/names")
public Flux<String> queryNmaes() {
return Flux.just("Hello", "World", "张三", "李四");
}
@GetMapping("/users")
public Mono<R<User>> queryUser() {
User user = new User();
user.setName("测试对象");
user.setAge(666);
return Mono.just(R.<User>builder().t(user)
.msg("success")
.build());
}
}
也可以向gin和flask等web框架链式注册路由
@Bean
public RouterFunction<ServerResponse> routes(WebHandler handler) {
return route()
.GET("/hello", handler::hello)
.build();
}
5.2 Reactive Programming API
还有一个和mvc的不同就是流式处理,相当于mvc同步模型的stream流采用, Spring WebFlux 通过 Reactor 框架实现响应式编程。常用的操作符有 map
, flatMap
, filter
, subscribe
等。
public Mono<String> getGreeting(String name) {
return Mono.just(name)
//数据格式转换
.map(n -> "Hello, " + n)
//一个数据转换为多个数据
.flatMap(greeting -> Mono.just(greeting + " from WebFlux!"));
}
是的,你的理解是正确的。在 Spring WebFlux 中,Flux
提供了一种响应式编程的方式,类似于 Java 8 中的流式编程,但它是针对异步数据流的处理。
Flux.merge 示例解析
Flux.merge()
是一个非常有用的方法,它可以将多个 Flux
实例合并成一个单一的 Flux
实例。它会将所有传入的 Flux
中的数据流汇聚到一起,并保持它们的异步特性。
下面我们详细解析一下你的示例代码:
Flux.merge(
Flux.just(1, 2, 3).delayElements(Duration.ofMillis(1)),
Flux.just(4, 5, 6).delayElements(Duration.ofMillis(1))
).subscribe(System.out::println);
- Flux.just
Flux.just(1, 2, 3)
和 Flux.just(4, 5, 6)
分别创建了两个 Flux
实例,分别包含了数字 1、2、3 和 4、5、6。
- delayElements
.delayElements(Duration.ofMillis(1))
会让每个元素在发出前延迟 1 毫秒。这个方法的作用是模拟数据流的延迟,这在测试异步行为时特别有用。
- Flux.merge
Flux.merge(...)
将两个 Flux
实例合并成一个新的 Flux
实例。这个新的 Flux
将会发出来自所有输入 Flux
的元素,而不需要等待它们完成。
- subscribe
最后,.subscribe(System.out::println)
表示对合并后的 Flux
进行订阅。每当有元素发出时,System.out::println
会被调用,从而打印出这些元素。类似foreach
示例输出
由于每个 Flux
都有 1 毫秒的延迟,输出将会是:
1
4
2
5
3
6
输出的顺序是交替的,因为 Flux.merge()
会将来自两个源的元素混合在一起。
- 其他常用的 Flux API
除了 merge
方法,Flux
还提供了许多其他有用的 API,以下是一些常用的方法:
-
map: 对每个元素应用一个函数,并返回一个新的
Flux
。//把流的原素依次变大俩倍 Flux<Integer> map = Flux.just(1, 2, 3) .map(i -> i * 2); map.subscribe(System.out::println);
-
filter: 只保留满足条件的元素。
Flux.just(1, 2, 3, 4, 5) .filter(i -> i % 2 == 0) // 输出: 2, 4 .subscribe(System.out::println);
-
flatMap: 将元素转换为多个元素的流,并将结果扁平化为一个
Flux
。Flux.just(1, 2, 3) .flatMap(i -> Flux.just(i, i * 10)) // 输出: 1, 10, 2, 20, 3, 30 .subscribe(System.out::println);
-
reduce: 对所有元素应用一个归约操作,最终返回一个单一结果。
Flux.just(1, 2, 3) .reduce(0, Integer::sum) // 输出: 6 .subscribe(System.out::println);
collect 收集流中的原素
Flux<Integer> numbersFlux = Flux.just(1, 2, 3);
Mono<List<Integer>> listMono = numbersFlux.collectList(); // 收集成 List
- 结合 Java 8 Stream API 的对比
- 流式编程:Java 的 Stream API 主要是同步的,它在操作数据时会阻塞当前线程,直到数据处理完成。
- 响应式编程:Spring WebFlux 中的
Flux
是异步的,可以在等待 I/O 操作的同时处理其他任务。它通过非阻塞的方式来提高系统的响应能力和处理能力。
- 小结
Spring WebFlux 的 API 和 Java 8 的流式 API 在语法上看起来相似,但在处理数据的方式上有很大的不同
6. 响应式与传统同步编程的区别
在非阻塞 I/O 模型中,当一个线程发出 I/O 请求后,它并不会阻塞等待结果,而是立即释放,去处理其他任务。而对于这个 I/O 请求的处理,通常有以下的机制来完成:
- I/O 操作交给操作系统或 I/O 子系统
当线程发起 I/O 请求时,具体的 I/O 操作(如网络通信、文件读取等)不会在应用层的线程中完成,而是由操作系统内核或I/O 子系统来负责。操作系统会负责执行实际的 I/O 操作,比如等待数据从网络中传输过来,或者等待磁盘完成读取。
- 在非阻塞模型中,操作系统会通过底层的机制(如
epoll
、select
、kqueue
等)来监听和管理 I/O 事件。当某个 I/O 操作完成时,操作系统会发出通知,告知应用层的线程可以继续处理接下来的步骤。
- 回调机制处理结果
当 I/O 操作完成时,操作系统会通知应用层,应用层通常通过回调机制处理后续的逻辑。回调函数可以在一个事件循环中被执行,或者在线程池中重新分配给某个线程来执行。此时,真正执行这部分代码的是一个事件驱动线程或一个新分配的线程,而不是原来发出 I/O 请求的线程。
- 回调机制是异步非阻塞 I/O 模型的核心。当 I/O 操作完成时,线程通过回调函数执行与该操作相关的业务逻辑。
- 谁执行回调代码?
-
事件循环线程:如果是像 Netty 这样的框架,它内部会维护一个事件循环(event loop),该循环由少量线程组成,负责监听 I/O 操作的完成事件。当操作系统通知某个 I/O 操作完成后,事件循环会选择一个空闲的线程去执行该 I/O 操作的后续处理代码(即回调函数)。
-
线程池中的线程:回调逻辑有时也可以交给一个线程池中的线程来处理。这种情况下,线程池中的某个空闲线程会执行回调函数,并继续完成 I/O 操作后的逻辑。
- 具体流程
举个例子来说明:
- 应用层线程发出一个网络请求,并调用 I/O 操作,例如从数据库读取数据。
- 该线程不等待读取结果,而是立即释放,继续处理其他任务。
- 操作系统负责真正的 I/O 操作(如等待数据库返回数据)。
- 一旦 I/O 操作完成,操作系统通过事件通知机制告知应用层 I/O 完成。
- 应用层的回调函数被触发,事件循环线程或线程池中的线程开始执行回调逻辑,处理 I/O 操作的结果。
6.1 线程池和并发处理
在 WebFlux 中,少量线程可以处理大量并发请求。通过异步 I/O 和事件驱动机制,线程池中的线程不会因为等待 I/O 操作而被阻塞,可以并行处理其他任务。
6.2 性能优化:减少线程上下文切换
传统的多线程阻塞 I/O 模型中,线程的上下文切换是性能瓶颈之一。WebFlux 通过少量线程高效处理 I/O 请求,减少了线程之间的切换和阻塞,从而提高了整体系统性能。
在传统的 Spring MVC 中,ThreadLocal
通常用于存储每个请求的用户上下文信息,因为每个请求都在独立的线程中处理,这样可以确保线程间不会相互干扰。然而,在 Spring WebFlux 中,由于采用了响应式编程模型和非阻塞 I/O,ThreadLocal
的使用并不适合,因为 WebFlux 使用的是反应式流,并且一个线程可能会处理多个请求。
WebFlux 中的上下文管理
在 WebFlux 中,由于不能使用threadlocal,所以使用 reactor.util.context.Context
来管理请求的上下文。这个上下文可以在异步执行的链中传递,确保每个请求在不同的上下文中运行,而不会互相影响。
使用 Context
存储用户上下文信息
-
创建过滤器:首先,你可以创建一个
WebFilter
,在其中提取用户上下文信息并将其存储到Context
中。 -
使用
deferContextual
读取上下文:在处理请求的过程中,可以通过Mono.deferContextual
方法读取当前的上下文。
示例代码
以下是一个如何在 WebFlux 中使用 Context
存储和获取用户上下文信息的示例:
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
@Component
public class UserContextFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// 从请求头中获取用户ID
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
// 将用户ID存储到上下文中
return chain.filter(exchange)
.contextWrite(Context.of("userId", userId)); // 将用户 ID 写入上下文
}
}
在服务或控制器中使用上下文
在服务或控制器中,你可以使用 deferContextual
来访问上下文中的用户信息:
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
@Service
public class UserService {
public Mono<String> getUserData() {
return Mono.deferContextual(context -> {
// 从上下文中获取用户ID
String userId = context.get("userId");
// 根据用户 ID 进行处理
return Mono.just("Data for user: " + userId);
});
}
}
在 Spring WebFlux 中,使用
Context
来存储和访问用户上下文信息,而不是使用ThreadLocal
。这种设计允许在异步和非阻塞的环境中有效地管理上下文,确保每个请求都在其独立的上下文中处理。这种方法避免了线程间的干扰,更加适合响应式编程模型。
7. Spring Security 与 WebFlux 的集成
Spring Security 提供了对 WebFlux 的支持,它也是非阻塞的,可以无缝集成到 WebFlux 中。
7.1 SecurityFilterChain 与 WebFlux
在 WebFlux 中,Spring Security 依赖 SecurityWebFilterChain
来处理请求。这个过滤器链是基于响应式流的,能够与异步 I/O 模型一起工作。
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange()
.pathMatchers("/admin/**").hasRole("ADMIN")
.anyExchange().authenticated()
.and().build();
}
7.2 上下文管理与异步安全处理
在 WebFlux 中,传统的 ThreadLocal
上下文管理方式不再适用,因为请求可能不会在同一个线程上完成。为了解决这个问题,WebFlux 提供了 Context
对象,可以在整个请求生命周期中传递安全上下文。
SecurityContext context = ReactiveSecurityContextHolder.getContext().block();
8. WebFlux 适用场景与性能分析
8.1 适用场景
- 高并发、大量 I/O 操作:WebFlux 非常适合处理高并发请求,尤其是在 I/O 操作密集型的场景中(如与数据库、文件系统、远程 API 的交互)。
- 流式数据处理:对于需要处理流式数据的应用,WebFlux 提供了强大的流处理能力(例如视频流、消息流等)。
特别是netty搭配websocket 这样上下文切换资源优化很多
8.2 性能优势
- 高效资源利用:通过事件驱动和非阻塞 I/O 模型,WebFlux 能够在少量线程的基础上处理大量并发请求,极大提升了资源的利用率。
- **减少
阻塞时间**:传统的线程池模型容易受限于线程上下文切换和阻塞 I/O 操作,WebFlux 通过减少这些瓶颈提高了系统的响应速度。
原文地址:https://blog.csdn.net/qq_55272229/article/details/142872219
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!