WebFlux 的简要介绍
WebFlux是 Spring 5 引入的一种新的反应式编程模型,用于构建异步、非阻塞的响应式应用程序。它是Spring框架对响应式编程的支持,以适应高并发、高吞吐量和低延迟的要求。WebFlux提供了一种基于反应式流(Reactive Streams)的方式来处理数据流,而不是传统的同步阻塞方式。
之所以尝试使用WebFlux是因为想通过WebSocket构建一个在线聊天室,而WebFlux默认支持WebSocket。
WebFlux 和 SpringMVC
- 编程模型:
- Spring MVC: 采用的是传统的同步阻塞模型,基于Servlet API。Controller中的处理方法是同步执行的,每个请求都会占用一个Servlet线程,如果线程阻塞,那么整个请求处理链也会被阻塞。
- WebFlux: 采用了反应式编程模型,使用Reactor库来实现异步和非阻塞。它允许开发者使用函数式编程风格处理异步事件,能够更好地处理高并发和实时数据。
- 线程模型:
- Spring MVC: 使用传统的Servlet线程模型,每个请求都会占用一个线程。
- WebFlux: 使用非阻塞的线程模型,避免了为每个请求分配一个线程的问题。在WebFlux中,请求不再由线程直接处理,而是通过_事件驱动_和_回调机制_进行处理。当有新的数据到达时,相应的回调函数会被触发,而不是等待线程阻塞。
- 适用场景:
- Spring MVC: 适用于传统的Web应用程序,如企业级应用、表单提交等,对于同步IO的应用场景较为合适。
- WebFlux: 适用于需要处理大量并发请求、实时数据的场景,对于异步IO和非阻塞IO的应用场景更为合适。
- 容器支持:
- Spring MVC: 主要运行在传统的Servlet容器上,如Tomcat、Jetty等。
- WebFlux: 可以运行在传统的Servlet容器上,也可以运行在支持Reactive Streams的容器上,如Netty。
- 路由方式:
- Spring MVC: 采用基于注解的声明式路由,通过@Controller和@RequestMapping来定义。
- WebFlux: 引入了一种新的声明式的路由方式,使用RouterFunctions和HandlerFunctions。
选用哪一种取决与业务需求,但个人想法是,Spring MVC 已经足够满足大多数人的日常开发了,除非是请求量过大的业务。而且两者可以一起用
WebFlux的处理模式
在WebFlux中,响应式编程采用了发布者-订阅者模式(Publisher-Subscriber Pattern)。这是一种用于处理异步数据流的模式,其中有一个发布者负责生成数据流,而一个或多个订阅者负责接收并处理这些数据。
- 发布者(Publisher):
- 发布者是负责产生数据流的组件。
- 在WebFlux中,发布者通常表示一个反应式流,可以是Mono(表示0或1个元素的流)或Flux(表示0到N个元素的流)。
- 发布者负责将数据推送给订阅者,可以是一次性的数据,也可以是持续的数据流。
- 订阅者(Subscriber):
- 订阅者是负责接收和处理数据的组件。
- 在WebFlux中,订阅者通常表示一个订阅了Mono或Flux的处理器或回调函数。
- 订阅者通过订阅发布者来表达对数据的兴趣,并在数据到达时接收和处理数据。
- 订阅者可以定义处理数据的回调方法,如
onNext
(处理每个元素)、onError
(处理错误)、onComplete
(处理流的完成)等。
- 订阅(Subscription):
- 订阅是发布者和订阅者之间的关联,表示订阅者对发布者的兴趣。
- 订阅可以被取消,这种取消是一种解除订阅关系的操作。
public class WebFluxExample {
public static void main(String[] args) {
// 创建一个Mono发布者
Mono<String> monoPublisher = Mono.create(monoSink -> {
// 发布者发出数据
monoSink.success("Hello, WebFlux!");
});
// 订阅者订阅发布者,并定义处理数据的回调方法
monoPublisher.subscribe(
// onNext回调,处理数据
data -> System.out.println("Received: " + data),
// onError回调,处理错误
error -> System.err.println("Error: " + error),
// onComplete回调,处理流的完成
() -> System.out.println("Processing completed")
);
}
}
函数式路由
在Spring WebFlux中,虽然还是可以使用
@Controller
和@Service
注解,但也引入了一种新的声明式路由方式,称为函数式路由。函数式路由不依赖于@Controller
和@Service
注解,而是使用Router Functions(路由器函数)和Handler Functions(处理器函数)来定义路由和处理逻辑。
@Configuration
public class WebFluxRouter {
@Bean
public RouterFunction<ServerResponse> route() {
return RouterFunctions.route()
.POST("/hello", this::handleHelloRequest)
.build();
}
private Mono<ServerResponse> handleHelloRequest(ServerRequest request) {
Mono<String> requestBody = request.bodyToMono(String.class);
return requestBody.flatMap(body -> {
String responseData = "Hello, " + body;
return ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromValue(responseData));
});
}
}
相较于注解驱动,函数式路由更适用于函数式编程和响应式编程的场景,而相较于函数式编程,注解驱动的可读性无疑更强,但在处理 异步等操作的时候就会比较复杂,对于习惯了注解驱动的人来说,学习函数式路由可能需要一些时间,因为它使用了不同的概念和编程范式。
假设我们有三个异步任务,分别是获取用户信息、获取商品信息和获取订单信息
@Service
public class AsyncService {
@Async
public CompletableFuture<String> getUserInfoAsync() {
// 模拟获取用户信息的异步操作
return CompletableFuture.completedFuture("User Info");
}
@Async
public CompletableFuture<String> getProductInfoAsync() {
// 模拟获取商品信息的异步操作
return CompletableFuture.completedFuture("Product Info");
}
@Async
public CompletableFuture<String> getOrderInfoAsync() {
// 模拟获取订单信息的异步操作
return CompletableFuture.completedFuture("Order Info");
}
}
注解驱动的实现方式
@RestController
@RequestMapping("/mvc")
public class AsyncController {
@Autowired
private AsyncService asyncService;
@GetMapping("/getData")
public ResponseEntity<String> getData() throws ExecutionException, InterruptedException {
CompletableFuture<String> userFuture = asyncService.getUserInfoAsync();
CompletableFuture<String> productFuture = asyncService.getProductInfoAsync();
CompletableFuture<String> orderFuture = asyncService.getOrderInfoAsync();
// 等待所有异步任务完成
CompletableFuture<Void> allOf = CompletableFuture.allOf(userFuture, productFuture, orderFuture);
allOf.join();
// 获取异步任务的结果
String userData = userFuture.get();
String productData = productFuture.get();
String orderData = orderFuture.get();
// 合并结果
String result = userData + "\n" + productData + "\n" + orderData;
return ResponseEntity.ok(result);
}
}
函数式路由实现方式
@Configuration
public class WebFluxRouter {
@Bean
public RouterFunction<ServerResponse> route(AsyncService asyncService) {
return RouterFunctions.route()
.GET("/webflux/getData", request -> handleGetData(request, asyncService))
.build();
}
private Mono<ServerResponse> handleGetData(ServerRequest request, AsyncService asyncService) {
Mono<String> userMono = asyncService.getUserInfoAsync();
Mono<String> productMono = asyncService.getProductInfoAsync();
Mono<String> orderMono = asyncService.getOrderInfoAsync();
// 合并异步任务的结果
return Mono.zip(userMono, productMono, orderMono)
.flatMap(tuple -> {
String userData = tuple.getT1();
String productData = tuple.getT2();
String orderData = tuple.getT3();
String result = userData + "\n" + productData + "\n" + orderData;
// 返回合并后的结果
return ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromValue(result));
});
}
}
异步非阻塞并不会使程序运行得更快。异步非阻塞模型并不能使接口的响应时间缩短,它仅仅能够提升吞吐量和伸缩性。
在传统的同步阻塞模型中,每个客户端连接通常都会分配一个线程来处理,如果某个连接的请求在处理过程中需要等待外部资源(比如数据库查询结果),那么这个线程就会被阻塞,无法处理其他连接的请求,但在异步非阻塞模型中,当某个连接的请求需要等待外部资源时,系统不会阻塞线程,而是将这个请求交给一个异步任务或者回调函数处理,并立即释放线程。这样,系统就可以在等待资源的同时继续处理其他连接的请求。
也就是说,异步非阻塞模型通过充分利用服务器资源,同时处理多个连接,从而间接性提高系统的并发处理能力和吞吐量。