1. 简介
Spring WebFlux 是基于响应式编程原理构建的新型函数式 Web 框架。本教程将深入探讨其实际应用场景。
我们将基于现有的 Spring 5 WebFlux 指南 进行扩展。在之前的指南中,我们使用基于注解的组件创建了一个简单的响应式 REST 应用。这里我们将采用函数式框架重新实现。
2. Maven 依赖
我们需要与前一篇文章相同的依赖项 *spring-boot-starter-webflux*:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>3.3.2</version>
</dependency>
3. 函数式 Web 框架
函数式 Web 框架引入了一种新的编程模型,我们使用函数来路由和处理请求。
与使用注解映射的注解模型不同,这里我们将使用 HandlerFunction 和 RouterFunction。
与注解控制器类似,函数式接口方法也构建在相同的响应式堆栈之上。
3.1 HandlerFunction
HandlerFunction 表示为路由到它的请求生成响应的函数:
@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
Mono<T> handle(ServerRequest request);
}
该接口本质上是一个 *Function<Request, Response
但与标准的 Servlet#service(ServletRequest req, ServletResponse res) 相比,HandlerFunction 不将响应作为输入参数。
3.2 RouterFunction
RouterFunction 是 @RequestMapping 注解的替代方案。我们可以用它将请求路由到处理函数:
@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {
Mono<HandlerFunction<T>> route(ServerRequest request);
// ...
}
通常,我们可以导入辅助函数 RouterFunctions.route() 来创建路由,而不是编写完整的路由器函数。
它允许我们通过应用 RequestPredicate 来路由请求。当谓词匹配时,返回第二个参数(处理函数):
public static <T extends ServerResponse> RouterFunction<T> route(
RequestPredicate predicate,
HandlerFunction<T> handlerFunction)
由于 route() 方法返回 RouterFunction,我们可以将其链接起来构建强大而复杂的路由方案。
4. 使用函数式 Web 构建响应式 REST 应用
在之前的指南中,我们使用 @RestController 和 WebClient 创建了一个简单的 EmployeeManagement REST 应用。
现在,让我们使用路由器函数和处理函数实现相同的逻辑。
首先,我们需要使用 RouterFunction 创建路由来发布和消费 Employee 的响应式流。
路由作为 Spring bean 注册,可以在任何配置类中创建。
4.1 单个资源
让我们使用 RouterFunction 创建第一个路由,用于发布单个 Employee 资源:
@Bean
RouterFunction<ServerResponse> getEmployeeByIdRoute() {
return route(GET("/employees/{id}"),
req -> ok().body(
employeeRepository().findEmployeeById(req.pathVariable("id")), Employee.class));
}
第一个参数是请求谓词。注意这里我们使用了静态导入的 RequestPredicates.GET 方法。第二个参数定义了当谓词适用时使用的处理函数。
换句话说,上述示例将所有对 /employees/{id} 的 GET 请求路由到 EmployeeRepository#findEmployeeById(String id) 方法。
4.2 集合资源
接下来,为了发布集合资源,我们添加另一个路由:
@Bean
RouterFunction<ServerResponse> getAllEmployeesRoute() {
return route(GET("/employees"),
req -> ok().body(
employeeRepository().findAllEmployees(), Employee.class));
}
4.3 单个资源更新
最后,我们添加一个更新 Employee 资源的路由:
@Bean
RouterFunction<ServerResponse> updateEmployeeRoute() {
return route(POST("/employees/update"),
req -> req.body(toMono(Employee.class))
.doOnNext(employeeRepository()::updateEmployee)
.then(ok().build()));
}
5. 组合路由
我们还可以在单个路由器函数中组合路由。
让我们看看如何组合上面创建的路由:
@Bean
RouterFunction<ServerResponse> composedRoutes() {
return
route(GET("/employees"),
req -> ok().body(
employeeRepository().findAllEmployees(), Employee.class))
.and(route(GET("/employees/{id}"),
req -> ok().body(
employeeRepository().findEmployeeById(req.pathVariable("id")), Employee.class)))
.and(route(POST("/employees/update"),
req -> req.body(toMono(Employee.class))
.doOnNext(employeeRepository()::updateEmployee)
.then(ok().build())));
}
这里我们使用了 RouterFunction.and() 来组合路由。
最后,我们使用路由器和处理函数实现了 EmployeeManagement 应用所需的完整 REST API。
要运行应用程序,我们可以使用单独的路由或上面创建的组合路由。
6. 测试路由
我们可以使用 WebTestClient 来测试路由。
为此,我们首先需要使用 bindToRouterFunction 方法绑定路由,然后构建测试客户端实例。
让我们测试 getEmployeeByIdRoute:
@Test
void givenEmployeeId_whenGetEmployeeById_thenCorrectEmployee() {
WebTestClient client = WebTestClient
.bindToRouterFunction(config.getEmployeeByIdRoute())
.build();
Employee employee = new Employee("1", "Employee 1");
given(employeeRepository.findEmployeeById("1")).willReturn(Mono.just(employee));
client.get()
.uri("/employees/1")
.exchange()
.expectStatus()
.isOk()
.expectBody(Employee.class)
.isEqualTo(employee);
}
类似地测试 getAllEmployeesRoute:
@Test
void whenGetAllEmployees_thenCorrectEmployees() {
WebTestClient client = WebTestClient
.bindToRouterFunction(config.getAllEmployeesRoute())
.build();
List<Employee> employees = Arrays.asList(
new Employee("1", "Employee 1"),
new Employee("2", "Employee 2"));
Flux<Employee> employeeFlux = Flux.fromIterable(employees);
given(employeeRepository.findAllEmployees()).willReturn(employeeFlux);
client.get()
.uri("/employees")
.exchange()
.expectStatus()
.isOk()
.expectBodyList(Employee.class)
.isEqualTo(employees);
}
我们还可以通过断言 Employee 实例已通过 EmployeeRepository 更新来测试 updateEmployeeRoute:
@Test
void whenUpdateEmployee_thenEmployeeUpdated() {
WebTestClient client = WebTestClient
.bindToRouterFunction(config.updateEmployeeRoute())
.build();
Employee employee = new Employee("1", "Employee 1 Updated");
client.post()
.uri("/employees/update")
.body(Mono.just(employee), Employee.class)
.exchange()
.expectStatus()
.isOk();
verify(employeeRepository).updateEmployee(employee);
}
有关使用 WebTestClient 测试的更多详细信息,请参阅我们关于 使用 WebClient 和 WebTestClient 的教程。
7. 总结
本教程介绍了 Spring 5 中的新型函数式 Web 框架,并深入研究了其两个核心接口——RouterFunction 和 HandlerFunction。我们还学习了如何创建各种路由来处理请求和发送响应。
此外,我们使用函数式接口模型重新创建了在 Spring 5 WebFlux 指南中介绍的 EmployeeManagement 应用程序。
完整的源代码可以在 GitHub 上找到。