1. Introduction

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

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.

2. Maven Dependency

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

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

3. Functional Web Framework

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.

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

3.1. HandlerFunction

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

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

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

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

3.2. RouterFunction

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

@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.

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

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.

4. Reactive REST Application Using Functional Web

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

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*.*

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

4.1. Single Resource

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

@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.

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

4.2. Collection Resource

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

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

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

5. Composing Routes

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.

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

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

6. Testing Routes

We can use WebTestClient to test our routes.

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

Let’s test our 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:

@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:

@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.

7. Summary

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.

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

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


« 上一篇: Apache Kafka与Spring