spring-5-functional-web
Introduction to the Functional Web Framework in Spring 5
1. Introduction
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
@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
@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
@Bean
RouterFunction<ServerResponse> getAllEmployeesRoute() {
return route(GET("/employees"),
req -> ok().body(
employeeRepository().findAllEmployees(), Employee.class));
}
5. Composing Routes
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
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.