Introduction to the Functional Web Framework in Spring 5 – Spring 5中的功能网络框架介绍

最后修改: 2017年 3月 29日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

Spring WebFlux is a new functional web framework built using reactive principles.

Spring WebFlux是一个使用反应式原则构建的新的功能性网络框架。

In this tutorial, we’ll learn how to work with it in practice.

在本教程中,我们将学习如何在实践中使用它。

We’ll base this off of our existing guide to Spring 5 WebFlux. In that guide, we created a simple reactive REST application using annotation-based components. Here, we’ll use the functional framework instead.

我们将以现有的Spring 5 WebFlux 指南为基础进行介绍。在该指南中,我们使用基于注解的组件创建了一个简单的反应式 REST 应用程序。在这里,我们将使用功能框架来代替。

2. Maven Dependency

2.Maven的依赖性

We’ll need the same spring-boot-starter-webflux dependency as defined in the previous article:

我们将需要相同的spring-boot-starter-webflux依赖关系,正如在上一篇文章中定义的那样。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>2.6.4</version>
</dependency>

3. Functional Web Framework

3.功能性网络框架

The functional web framework introduces a new programming model where we use functions to route and handle requests.

函数式网络框架引入了一种新的编程模型,我们使用函数来路由和处理请求。

As opposed to the annotation-based model where we use annotations mappings, here we’ll use HandlerFunction and RouterFunctions.

相对于基于注解的模型,我们使用注解映射,这里我们将使用HandlerFunctionRouterFunctionS

Similarly, as in the annotated controllers, the functional endpoints approach is built on the same reactive stack.

同样地,与注解控制器一样,功能端点方法也是建立在相同的反应式堆栈上。

3.1. HandlerFunction

3.1.HandlerFunction

The HandlerFunction represents a function that generates responses for requests routed to them:

HandlerFunction代表一个函数,为路由到它们的请求生成响应。

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

This interface is primarily a Function<Request, Response<T>>, which behaves very much like a servlet.

这个接口主要是一个Function<Request, Response<T>>,它的行为很像一个Servlet。

Although, compared to a standard Servlet#service(ServletRequest req, ServletResponse res), HandlerFunction doesn’t take a response as an input parameter.

尽管与标准的Servlet#service(ServletRequest req, ServletResponse res)相比,HandlerFunction并不接受响应作为输入参数。

3.2. RouterFunction

3.2.RouterFunction

RouterFunction serves as an alternative to the @RequestMapping annotation. We can use it to route requests to the handler functions:

RouterFunction@RequestMapping注解的替代品。我们可以用它来将请求路由到处理函数。

@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {
    Mono<HandlerFunction<T>> route(ServerRequest request);
    // ...
}

Typically, we can import the helper function RouterFunctions.route()  to create routes, instead of writing a complete router function.

通常,我们可以导入辅助函数RouterFunctions.route()来创建路由,而不是编写一个完整的路由器函数。

It allows us to route requests by applying a RequestPredicate. When the predicate is matched, then the second argument, the handler function, is returned:

它允许我们通过应用一个RequestPredicate来路由请求。当谓词被匹配时,第二个参数,即处理函数,将被返回。

public static <T extends ServerResponse> RouterFunction<T> route(
  RequestPredicate predicate,
  HandlerFunction<T> handlerFunction)

Because the route() method returns a RouterFunction, we can chain it to build powerful and complex routing schemes.

因为route()方法返回一个RouterFunction,所以我们可以通过连锁来构建强大而复杂的路由方案。

4. Reactive REST Application Using Functional Web

4.使用功能网络的反应式REST应用程序

In our previous guide, we created a simple EmployeeManagement REST application using @RestController and WebClient.

在之前的指南中,我们使用@RestControllerWebClient创建了一个简单的EmployeeManagement REST应用程序。

Now, let’s implement the same logic using router and handler functions.

现在,让我们用路由器和处理函数来实现同样的逻辑。

First, we need to create routes using RouterFunction to publish and consume our reactive streams of Employees.

首先,我们需要使用RouterFunction创建路由,以发布和消费我们的Employees反应式流。

Routes are registered as Spring beans and can be created inside any configuration class.

路由被注册为Spring Bean,可以在任何配置类中创建。

4.1. Single Resource

4.1.单一资源

Let’s create our first route using RouterFunction that publishes a single Employee resource:

让我们使用RouterFunction创建我们的第一个路由,发布一个单一的Employee资源。

@Bean
RouterFunction<ServerResponse> getEmployeeByIdRoute() {
  return route(GET("/employees/{id}"), 
    req -> ok().body(
      employeeRepository().findEmployeeById(req.pathVariable("id")), Employee.class));
}

The first argument is a request predicate. Notice how we used a statically imported RequestPredicates.GET method here. The second parameter defines a handler function that’ll be used if the predicate applies.

第一个参数是一个请求谓词。注意我们在这里使用了一个静态导入的RequestPredicates.GET方法。第二个参数定义了一个处理函数,如果该谓词适用,将被使用。

In other words, the above example routes all the GET requests for /employees/{id} to EmployeeRepository#findEmployeeById(String id) method.

换句话说,上面的例子将所有对/employees/{id}的GET请求路由到EmployeeRepository#findEmployeeById(String id)方法。

4.2. Collection Resource

4.2.采集资源

Next, for publishing a collection resource, let’s add another route:

接下来,为了发布一个集合资源,让我们再添加一条路线。

@Bean
RouterFunction<ServerResponse> getAllEmployeesRoute() {
  return route(GET("/employees"), 
    req -> ok().body(
      employeeRepository().findAllEmployees(), Employee.class));
}

4.3. Single Resource Update

4.3.单一资源更新

Lastly, let’s add a route for updating the Employee resource:

最后,让我们为更新Employee资源添加一个路由。

@Bean
RouterFunction<ServerResponse> updateEmployeeRoute() {
  return route(POST("/employees/update"), 
    req -> req.body(toMono(Employee.class))
      .doOnNext(employeeRepository()::updateEmployee)
      .then(ok().build()));
}

5. Composing Routes

5.构成路线

We can also compose the routes together in a single router function.

我们也可以在一个单一的路由器函数中把这些路由组合在一起。

Let’s see how to combine the routes created above:

让我们看看如何结合上面创建的路线。

@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())));
}

Here, we’ve used RouterFunction.and() to combine our routes.

在这里,我们使用RouterFunction.and()来组合我们的路由。

Finally, we’ve implemented the complete REST API needed for our EmployeeManagement application, using routers and handlers.

最后,我们使用路由器和处理程序实现了我们的EmployeeManagement应用程序所需的完整REST API。

To run the application, we can either use separate routes or the single, composed one that we created above.

为了运行应用程序,我们可以使用单独的路由,也可以使用我们上面创建的单一的、组成的路由。

6. Testing Routes

6.测试路线

We can use WebTestClient to test our routes.

我们可以使用WebTestClient 来测试我们的路由。

To do so, we first need to bind the routes using the bindToRouterFunction method and then build the test client instance.

为此,我们首先需要使用bindToRouterFunction方法绑定路由,然后构建测试客户端实例。

Let’s test our getEmployeeByIdRoute:

我们来测试一下我们的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);
}

and similarly getAllEmployeesRoute:

和类似的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);
}

We can also test our updateEmployeeRoute by asserting that our Employee instance is updated via EmployeeRepository:

我们还可以通过断言我们的updateEmployeeRoute实例通过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);
}

For more details on testing with WebTestClient please refer to our tutorial on working with WebClient and WebTestClient.

关于使用WebTestClient进行测试的更多细节,请参考我们关于使用WebClientWebTestClient的教程。

7. Summary

7.总结

In this tutorial, we introduced the new functional web framework in Spring 5 and looked into its two core interfaces – RouterFunction and HandlerFunction. We also learned how to create various routes to handle the request and send the response.

在本教程中,我们介绍了Spring 5中新的功能型Web框架,并研究了它的两个核心接口–RouterFunction HandlerFunction。我们还学习了如何创建各种路由来处理请求并发送响应。

Additionally, we recreated our EmployeeManagement application introduced in guide to Spring 5 WebFlux with the functional endpoints model.

此外,我们用功能端点模型重新创建了Spring 5 WebFlux指南中介绍的EmployeeManagement应用。

As always, the full source code can be found over on Github.

一如既往,完整的源代码可以在Github上找到over