1,响应式编程
1.1 快速理解
在现在主流的编程模型中,请求是被同步阻塞处理完成,返回结果给前端。
在响应式的编程模型中,请求是被作为一个事件丢到线程池中执行,等到执行完毕,异步回调结果给主线程,最后返回给前端。
通过这样的方式,主线程(实际是多个,这里只是方便描述哈)不断接收请求,不负责直接同步阻塞处理,从而避免自身被阻塞。
1.2 Reactor 框架
在 Java 生态中,提供响应式编程的框架主要有 Reactor、RxJava、JDK9 Flow API 。
Reactor 说是一个响应式编程框架,又快又不占用内存的那种。
Reactor 有两个非常重要的基本概念:
Flux ,表示的是包含 0 到 N 个元素的异步序列。当消息通知产生时,订阅者(Subscriber)中对应的方法 #onNext(t),#onComplete(t) 和 #onError(t) 会被调用。
Mono 表示的是包含 0 或者 1 个元素的异步序列。
该序列中同样可以包含与 Flux 相同的三种类型的消息通知。
同时,Flux 和 Mono 之间可以进行转换。
例如: 对一个 Flux 序列进行计数操作,得到的结果是一个 Mono 对象。
把两个 Mono 序列合并在一起,得到的是一个 Flux 对象。
其实,可以先暂时简单把 Mono 理解成 Object ,Flux 理解成 List 。
1.3 Spring WebFlux
Spring 官方文档对 Spring WebFlux 介绍总结:
Spring Framework 5 提供了一个新的 spring-webflux 模块。该模块包含了:
- 对响应式支持的 HTTP 和 WebSocket 客户端。
- 对响应式支持的 Web 服务器,包括 Rest API、HTML 浏览器、WebSocket 等交互方式。
在服务端方面,WebFlux 提供了 2 种使用方式:
- 方式一,基于 Annotated Controller 方式实现:基于 @Controller 和 SpringMVC使用的其它注解。也就是说,我们大体上可以像使用 SpringMVC 的方式,使用 WebFlux 。
- 方式二,基于函数式编程方式:函数式,Java 8 lambda 表达式风格的路由和处理。
WebFlux 可以运行在:
- 支持 Servlet 3.1 非阻塞 IO API 的 Servlet 容器上
- 也可以运行在支持异步运行时的,例如说 Netty 或者 Undertow 上
每一个运行时(runtime)适用于将响应式的 ServerHttpRequest 和 ServerHttpResponse 中 request 和 response 的 body 暴露成 Flux 对象,而不是 InputStream 和InputStream 对象,可用于响应式中的背压(backpressure)。
简单来说:
- 对于 Servlet 来说, ServletRequest#getInputStream() 方法,获得请求的主体内容返回的是InputStream 对象。
- 对于 WebFlux 来说,ServerHttpRequest#getBody()方法,获得请求的主体内容返回的是 Flux 对象。
2 快速入门
本小节,我们会使用 spring-boot-starter-webflux 实现 WebFlux 的自动化配置。然后实现用户的增删改查接口。接口列表如下:
请求方法 | URL | 功能 |
---|---|---|
GET | /users/list | 查询用户列表 |
GET | /users/get | 获得指定用户编号的用户 |
POST | /users/add | 添加用户 |
POST | /users/update | 更新指定用户编号的用户 |
POST | /users/delete | 删除指定用户编号的用户 |
2.1 引入依赖
在 pom.xml 文件中,引入相关依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>lab-27-webflux-01</artifactId>
<dependencies>
<!-- 实现对 Spring WebFlux 的自动化配置 -->
<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>
</dependencies>
</project>
我们使用 IDEA Maven 插件 ,查看下== spring-boot-starter-webflux ==依赖中,所引入的依赖。如下图所示:
- 引入 reactor-core 依赖,使用 Reactor 作为 WebFlux 的响应式框架的基础。
- 引入 spring-boot-starter-reactor-netty 依赖,使用 Netty 构建 WebFlux 的 Web 服务器。其中 RxNetty 库,是基于 Reactor 的响应式框架的基础之上,提供出 Netty 的响应式 API 。
为什么 WebFlux 运行在 Servlet 容器上时,需要 Servlet 3.1+ 以上的容器呢?在 Servlet 3.1 规范发布时,它定义了非常重要的特性,Non-blocking I/O 非阻塞 IO ,提供了异步处理请求的支持。我们来详细展开下:
- 在 Servlet 3.1 规范之前的版本,请求是只能被 Servlet 同步阻塞处理完成,返回结果给前端。
- 在 Servlet 3.1 规范开始的版本,请求是允许被 Servlet 丢到线程池中处执行,等到执行完毕,异步回调结果给 Servlet ,最后返回给前端。
可以看看 《Servlet 3.0/3.1 中的异步处理》 文章,可以对 WebFlux 有更好的理解。
2.2 基于 Annotated Controller 方式实现
在 cn.iocoder.springboot.lab27.springwebflux.controller 包路径下,创建 UserController 类。代码如下:
/ UserController.java
@RestController
@RequestMapping("/users")
public class UserController {
/**
* 查询用户列表
*
* @return 用户列表
*/
@GetMapping("/list")
public Flux<UserVO> list() {
// 查询列表
List<UserVO> result = new ArrayList<>();
result.add(new UserVO().setId(1).setUsername("yudaoyuanma"));
result.add(new UserVO().setId(2).setUsername("woshiyutou"));
result.add(new UserVO().setId(3).setUsername("chifanshuijiao"));
// 返回列表
return Flux.fromIterable(result);
}
/**
* 获得指定用户编号的用户
*
* @param id 用户编号
* @return 用户
*/
@GetMapping("/get")
public Mono<UserVO> get(@RequestParam("id") Integer id) {
// 查询用户
UserVO user = new UserVO().setId(id).setUsername("username:" + id);
// 返回
return Mono.just(user);
}
/**
* 添加用户
*
* @param addDTO 添加用户信息 DTO
* @return 添加成功的用户编号
*/
@PostMapping("add")
public Mono<Integer> add(@RequestBody Publisher<UserAddDTO> addDTO) {
// 插入用户记录,返回编号
Integer returnId = 1;
// 返回用户编号
return Mono.just(returnId);
}
/**
* 更新指定用户编号的用户
*
* @param updateDTO 更新用户信息 DTO
* @return 是否修改成功
*/
@PostMapping("/update")
public Mono<Boolean> update(@RequestBody Publisher<UserUpdateDTO> updateDTO) {
// 更新用户记录
Boolean success = true;
// 返回更新是否成功
return Mono.just(success);
}
/**
* 删除指定用户编号的用户
*
* @param id 用户编号
* @return 是否删除成功
*/
@PostMapping("/delete") // URL 修改成 /delete ,RequestMethod 改成 DELETE
public Mono<Boolean> delete(@RequestParam("id") Integer id) {
// 删除用户记录
Boolean success = false;
// 返回是否更新成功
return Mono.just(success);
}
}
list() 方法,我们最终调用 Flux#fromIterable(Iterable<? extends T> it) 方法,将 List 包装成 Flux 对象返回。
get(Integer id) 方法,我们最终调用 Mono#just(T data) 方法,将 UserVO 包装成 Mono 对象返回。add(Publisher addDTO) 方法,参数为 Publisher 类型,泛型为 UserAddDTO 类型,并且添加了 @RequestBody 注解,从 request 的 Body
中读取参数。注意,此时提交参数需要使用 “application/json” 等 Content-Type 内容类型。add(…) 方法,也可以使用 application/x-www-form-urlencoded 或 multipart/form-data 这两个 Content-Type 内容类型,通过 request 的 Form Data 或
Multipart Data 传递参数。代码如下:
// UserController.java
/**
* 添加用户
*
* @param addDTO 添加用户信息 DTO
* @return 添加成功的用户编号
*/
@PostMapping("add2")
public Mono<Integer> add(Mono<UserAddDTO> addDTO) {
// 插入用户记录,返回编号
Integer returnId = UUID.randomUUID().hashCode();
// 返回用户编号
return Mono.just(returnId);
}
此时,参数为 Mono 类型,泛型为 UserAddDTO 类型。
当然,我们也可以直接使用参数为 UserAddDTO 类型。如果后续需要使用到 Reactor API ,则我们自己主动调用 Mono#just(T data) 方法,封装出 Publisher 对象。注意,Flux 和 Mono 都实现了 Publisher 接口。
2.3 基于函数式编程方式
在== cn.iocoder.springboot.lab27.springwebflux.controller== 包路径下,创建 UserRouter 类。代码如下:
// UserRouter.java
@Configuration
public class UserRouter {
@Bean
public RouterFunction<ServerResponse> userListRouterFunction() {
return RouterFunctions.route(RequestPredicates.GET("/users2/list"),
new HandlerFunction<ServerResponse>() {
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
// 查询列表
List<UserVO> result = new ArrayList<>();
result.add(new UserVO().setId(1).setUsername("yudaoyuanma"));
result.add(new UserVO().setId(2).setUsername("woshiyutou"));
result.add(new UserVO().setId(3).setUsername("chifanshuijiao"));
// 返回列表
return ServerResponse.ok().bodyValue(result);
}
});
}
@Bean
public RouterFunction<ServerResponse> userGetRouterFunction() {
return RouterFunctions.route(RequestPredicates.GET("/users2/get"),
new HandlerFunction<ServerResponse>() {
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
// 获得编号
Integer id = request.queryParam("id")
.map(s -> StringUtils.isEmpty(s) ? null : Integer.valueOf(s)).get();
// 查询用户
UserVO user = new UserVO().setId(id).setUsername(UUID.randomUUID().toString());
// 返回列表
return ServerResponse.ok().bodyValue(user);
}
});
}
@Bean
public RouterFunction<ServerResponse> demoRouterFunction() {
return route(GET("/users2/demo"), request -> ok().bodyValue("demo"));
}
}
-
在类上,添加== @Configuration== 注解,保证该类中的 Bean 们,都被扫描到。
-
在每个方法中,我们都通弄 RouterFunctions#route(RequestPredicate predicate, HandlerFunction handlerFunction) 方法,定义了一条路由。
- 第一个参数 predicate 参数,是 RequestPredicate 类型,请求谓语,用于匹配请求。可以通过 RequestPredicates 来构建各种条件。
- 第二个参数 handlerFunction 参数,是 RouterFunction 类型,处理器函数。
-
采用第三个方法的写法,更加简洁。注意,需要使用 static import 静态引入,代码如下
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.*;
import static org.springframework.web.reactive.function.server.ServerResponse.*;