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 框架引入了一种新的编程模型,我们使用函数来路由和处理请求。

与使用注解映射的注解模型不同,这里我们将使用 HandlerFunctionRouterFunction

与注解控制器类似,函数式接口方法也构建在相同的响应式堆栈之上。

3.1 HandlerFunction

HandlerFunction 表示为路由到它的请求生成响应的函数:

@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
    Mono<T> handle(ServerRequest request);
}

该接口本质上是一个 *Function<Request, Response>*,其行为非常类似于 servlet。

但与标准的 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 应用

在之前的指南中,我们使用 @RestControllerWebClient 创建了一个简单的 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 测试的更多详细信息,请参阅我们关于 使用 WebClientWebTestClient 的教程。

7. 总结

本教程介绍了 Spring 5 中的新型函数式 Web 框架,并深入研究了其两个核心接口——RouterFunctionHandlerFunction。我们还学习了如何创建各种路由来处理请求和发送响应。

此外,我们使用函数式接口模型重新创建了在 Spring 5 WebFlux 指南中介绍的 EmployeeManagement 应用程序。

完整的源代码可以在 GitHub 上找到。


原始标题:Introduction to the Functional Web Framework in Spring | Baeldung