Introduction to the Functional Web Framework in Spring 5

1. Introduction

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

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

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

2. Maven Dependency

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

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

3. Functional Web Framework

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.

Also, the functional web framework is built on the same reactive stack on which annotation-based reactive framework was built upon.

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<T>>, which behaves very much like a servlet.

Although, compared to a standard servlet Servlet.service(ServletRequest req, ServletResponse res), HandlerFunction returns the response instead of taking it as a parameter which makes it side-effect free and easier to test and reuse.

3.2. RouterFunction

RouterFunction serves as an alternative to the @RequestMapping annotation. It’s used for routing incoming requests to handler functions:

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

Typically, we can import RouterFunctions.route(), a helper function 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)

By returning a RouterFunction, route() can be chained and nested to build powerful and complex routing schemes.

4. Reactive REST Application Using Functional Web

In our guide to Spring WebFlux tutorial, we create a small EmployeeManagement REST application using annotated @RestController and WebClient. 

Now, let’s build the same application using Router and Handler functions.

To begin with, we’ll create routes using RouterFunction to publish and consume our reactive streams of Employee. Routes are registered as Spring beans and can be created inside any class that will be used as Spring configuration.

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

To break it down, the first argument defines an HTTP GET request that’ll invoke the handler. I.e. return matched Employee from employeeRepository only if the path is /employee/{id} and the request is of type GET.

4.2. Collection Resource

Next, for publishing 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 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 combine our 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 have used RouterFunction.and() to combine our routes.

Finally, we have created all the REST APIs using Router and Handler that we needed for our EmployeeManagement application.  To run our application we can either use different routes or a single composed one that we created above.

6. Testing Routes

We can use WebTestClient to test our routes. 

To test our routes with WebTestClient we need to bind our routes using bindToRouterFunction and build our test client instance.

Let’s test our getEmployeeByIdRoute:

@Test
public void givenEmployeeId_whenGetEmployeeById_thenCorrectEmployee() {
    WebTestClient client = WebTestClient
        .bindToRouterFunction(config.getEmployeeByIdRoute())
        .build();

    Employee expected = 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(expected);
}

and similarly getAllEmployeesRoute:

@Test
public void whenGetAllEmployees_thenCorrectEmployees() {
    WebTestClient client = WebTestClient
        .bindToRouterFunction(config.getAllEmployeesRoute())
        .build();

    List<Employee> employeeList = new ArrayList<>();

    Employee employee1 = new Employee("1", "Employee 1");
    Employee employee2 = new Employee("2", "Employee 2");

    employeeList.add(employee1);
    employeeList.add(employee2);

    Flux<Employee> employeeFlux = Flux.fromIterable(employeeList);
    given(employeeRepository.findAllEmployees()).willReturn(employeeFlux);

    client.get()
        .uri("/employees")
        .exchange()
        .expectStatus()
        .isOk()
        .expectBodyList(Employee.class)
        .isEqualTo(employeeList);
}

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

@Test
public 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, we looked into its two core functions Router and Handler and we learned how to create various routes to handle the request and send the response.

Also, we recreated our EmployeeManagement application introduced in Guide to Spring 5 WebFlux with our new framework.

Laying its foundation on Reactor, the reactive framework would fully shine with reactive access to data stores. Unfortunately, most data stores do not provide such reactive access yet, except for a few NoSQL databases such as MongoDB.

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

Leave a Reply

Your email address will not be published.