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>2.6.4</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
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.