Using Functional Web Framework in Spring WebFlux Application

Example of implementing Functional Web Framework in a Spring 5 WebFlux Application. Learn to use Functional Style Routers and Handlers in WebFlux to build a Reactive REST Service.

Overview

Functional Web Framework supports Java functional programming style request routing and handling. Unlike the static request mapping of Controllers, functional style routing is flexible and extensible.

In this tutorial we will cover an example of creating reactive functional style routes using Spring Functional Web Framework and WebFlux. We’ll start from scratch to create an application, build reactive endpoints and write tests.

For a detailed understanding of Spring 5 Functional Web Framework, read Functional Web Framework Guide.

Spring 5 Functional Web Framework Example

Using Spring WebFlux, we can build reactive non-blocking web applications. We have already seen an example of building Controller based Reactive REST Service in Spring WebFlux. For this tutorial, we use the same context and built a reactive REST service using Functional Routing and Handlers.

We will create a Students service with a basic REST endpoints to retrieve or to create new Students reactively. In order to demonstrate effective usage of the most common publishers – Flux and Mono, we will consider next three endpoints.

  • GET /students
  • GET /students/{id}
  • POST /students

Also, for the sake of simplicity we will consider a basic Model with only a few fields. Our Student model looks like this.

public class Student { private Long studentId; private String firstName; private String lastName; private Integer year; // Constructors // Getter & Setter Methods }
Code language: Java (java)

Let’s build a Web Service using this model from scratch.

Dependency

Firstly, create a basic Spring Boot project with all basic components. The easiest way to build a Spring Boot application is to use Spring Initializr portal. While doing so, it is always a good practice use to use the latest stable versions of different components, unless there are specific needs.

Once the project is ready, we need to add Spring WebFlux dependency. This is done by adding webflux starter dependency into build.xml (maven) or build.gradle (gradle) file.

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency>
Code language: HTML, XML (xml)

The spring-boot-starter-webflux dependency implicitly brings in other required dependencies. It is important to note that, this dependency also installs Netty – a reactive web server and not tomcat at it would do for any other spring boot application. Also, this dependency installs spring-web package that provides the Functional Web Framework.

Service Class

Before we write the request router functions and handlers, we will create the service class. As we are developing a reactive web service, the service class will return publishers of the result – i.e. Flux or Mono.

Student Service

@Service public class StudentService { public Mono<Student> getStudent(Long id) { return Mono.just(new Student(id, "fName", "lName", 2030)); } public Flux<Student> getStudents() { return Flux.just(new Student(111L, "fName1", "lName1", 2030), new Student(112L, "fName2", "lName2", 2031)); } public Mono<Student> addStudent(Mono<Student> student) { return student; } }
Code language: Java (java)

In this tutorial, we wan to focus more on Router Functions in a Reactive REST Service. Thus we will have the service return a mock data.

Routing Functions and Handlers

Routing functions are equivalent to Controllers. However, we need to define these functions in @Bean Factory method. Thus, we will create a @Configuration class that contains all the routing function factories.

@Configuration public class StudentRouterFunctionConfig { private final StudentService service; }
Code language: Java (java)

The StudentRouterFunctionConfig has a reference to StudentService. The handlers in the routers will delegate processing to this service class.

GET Resource Collection

We will create our first Routing Function to publish a collection of students in the StudentRouterFunctionConfig.java.

StudentRouterFunctionConfig#getStudentRoute()

@Bean public RouterFunction<ServerResponse> getStudentsRoute() { return route(GET("/students"), request -> ok().body(service.getStudents(), Student.class)); }
Code language: Java (java)

The snippet shows an example of a RouterFunction with a path based RequestPredicate to match “/students“. The router function also defines a request handler.

The lambda style handler invokes the service method. Then, it creates a new ServerResponse instance with status code of 200 and inserts the body.

GET Single Resource

Similarly, we will create a functional route for a single resource GET endpoint and add it to the StudentRouterFunctionConfig class. For the sake of brevity of the syntax, we will use static imports wherever possible.

StudentRouterFunctionConfig#getStudentRoute()

@Bean public RouterFunction<ServerResponse> getStudentRoute() { return route(GET("/students/{id}"), request -> { Long id = parseLong(request.pathVariable("id")); return ok().body(service .getStudent(id), Student.class); } ); }
Code language: Java (java)

This snippet is a RouterFunction factory that accepts a single Resource path – /students/{id}, along with a handler. Firstly, the handler reads the path variable from the request and parse it to a Long. Next is invokes the service method and provides the Id value. Finally, it generates a new ServerResponse instance with the student returned by the service.

POST a Resource

So far, we have implemented two GET endpoints using Functional Routing. Now, let’s try an example of creating Router Function to handle a POST request.

@Bean public RouterFunction<ServerResponse> addStudentRoute() { return route(POST("/students"), request -> { Mono<Student> student = request.body(toMono(Student.class)); return ok().body(service.addStudent(student), Student.class); } ); }
Code language: Java (java)

As seen the request predicate in the router function defines a POST endpoint. While the handler extract the body content (using BodyExtractors) and convert it to a Mono of Student bean. Finally it passes the publisher to service method and generates response.

Testing Router Functions

So far, we have a working reactive rest application that is backed by functional web framework. The application supports two GET and a POST endpoints. In this section, we will learn how write SpringBootTest to test the routes.

Create a Test Class

In order to test the reactive APIs, we will use WebTestClient. This test client provides useful APIs to perform tests as well as verify results. Before that, we will create a basic test class using @SpringBootTest.

@SpringBootTest(webEnvironment = RANDOM_PORT) public class StudentRouterFunctionConfigTest { @Autowired private StudentRouterFunctionConfig config; @MockBean private StudentService service; ... }
Code language: Java (java)

Note that, for the test we will be executing the actual routes but provide a mock to the service class.

Test GET Resource Collection Route

Next, is an example of testing GET /student endpoint, where we will mock the service method to return a specific data.

@Test public void getStudentsCorrectlyReturnsStudents() { WebTestClient testClient = WebTestClient .bindToRouterFunction(config.getStudentsRoute()) .build(); List<Student> students = List.of( new Student(11L, "f1", "l1", 1920), new Student(12L, "f2", "l2", 1921) ); when(service.getStudents()).thenReturn(Flux.fromIterable(students)); testClient.get().uri("/students") .exchange() .expectStatus().is2xxSuccessful() .expectBodyList(Student.class) .isEqualTo(students); }
Code language: Java (java)

Firstly, we created a WebTestClient instance by providing the router function under test. Next, we mocked the service method to make it return a fixed set of results. Lastly, we executed the GET /students call, verified if it is successful, and also verified if it returns the same collection of students.

Test GET Single Resource Route

Next, we will will follow similar approach to test the route that return a single resource.

@Test public void getStudentCorrectlyReturnsStudentById() { WebTestClient testClient = WebTestClient .bindToRouterFunction(config.getStudentRoute()) .build(); Long id = 12345L; Student student = new Student(id, "f1", "l1", 1930); when(service.getStudent(anyLong())).thenReturn(Mono.just(student)); testClient.get().uri("/students/" + id) .exchange() .expectStatus().is2xxSuccessful() .expectBody(Student.class) .isEqualTo(student); verify(service).getStudent(id); }
Code language: Java (java)

As shown in the snippet, we have created a dummy Student and mocked the service to return the dummy student whenever getStudent(anyLong) method is invoked. By the end of the execution of the REST call we verify if the same dummy student is returned by the routes. In addition, we also verify the service method was invoked once with correct argument.

Test POST Resource Route

Lastly, we will test the POST /students endpoint like we tested the others. However, this time we will need to add a student to request body.

@Test public void addStudentSaveStudentCorrectly() { WebTestClient testClient = WebTestClient .bindToRouterFunction(config.addStudentRoute()) .build(); Student student = new Student(1234L, "f1", "l1", 1930); when(service.addStudent(any(Mono.class))).thenReturn(Mono.just(student)); testClient.post().uri("/students") .body(Mono.just(student), Student.class) .exchange() .expectStatus().is2xxSuccessful() .expectBody(Student.class) .isEqualTo(student); }
Code language: Java (java)

Note, that we have used the post() method here and provided the request body using the body() method. Lastly, we also verified the student returned in the response is the correct one.

Summary

That, was a complete example of Functional Web Framework to create Reactive Web Service. In this practical tutorial we followed a step-by-step approach to create Reactive Functional Routes from scratch. We also tested all the three reactive functional routes using WebTestClient.