Spring framework version 5 with support from Java 8 and above, can help us using Functional Programming in Java code. And Spring WebFlux is no exception. We can also build Reactive web applications using Lambda Expression. In this tutorial, I will guide you all how to use Functional Programming with Spring WebFlux.
First, I will also create a new Spring Boot project with Reactive Web support using Spring Initializr Web as an example:
If you do not know how to create Spring Boot project with Spring Initializr Web, you can refer to this tutorial.
Note that, after creating the project, you should open the Maven pom.xml file and remove the dependency spring-boot-starter-web:
1 2 3 4 |
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> |
Otherwise we will not run the example.
As in the tutorial about Spring WebFlux using annotation, in this tutorial, I will also create an application that provides list of student with data added every second. When a user requests to our application, whenever a new student is added, the student’s information is published to the user.
Student information will be stored in the Student object as follows:
1 2 3 4 5 6 7 8 9 10 11 |
package com.huongdanjava.springwebfluxfunctional; import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor public class Student { private String name; } |
Here, I also used Project Lombok.
1 2 3 4 5 6 |
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.18</version> <scope>provided</scope> </dependency> |
For Functional Programming in Spring WebFlux, instead of using annotations in Spring MVC, we will now use the HandlerFunction with Lambda Expression to process the request and map request URL to the HandlerFunction object using the RouterFunction object.
Here, we can see the RouterFunction like the @RequestMapping annotation that defines the request URL, the HTTP method, and so on. HandlerFunction is the method declared with the @RequestMapping annotation in Spring MVC.
- The HandlerFunction is a Functional Interface that will process the request from the user from the ServerRequest object and return the client object ServerResponse.
The content of this interface is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.web.reactive.function.server; import reactor.core.publisher.Mono; /** * Represents a function that handles a {@linkplain ServerRequest request}. * * @author Arjen Poutsma * @since 5.0 * @param <T> the type of the response of the function * @see RouterFunction */ @FunctionalInterface public interface HandlerFunction<T extends ServerResponse> { /** * Handle the given request. * @param request the request to handle * @return the response */ Mono<T> handle(ServerRequest request); } |
Here, the ServerRequest and ServerResponse objects are new objects introduced with Spring WebFlux to hold information about the request and response of a request URL in Reactive web applications. We can get Mono or Flux objects in Project Reactor from these objects.
In the example of this tutorial, I will create a HandlerFunction with the following contents:
1 |
HandlerFunction<ServerResponse> handlerFunction = (request) -> ServerResponse.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(all(), Student.class); |
The ServerResponse.ok () method means: we will return the result to the client with HTTP status code of 200. Here, we also use the Content-Type “text/event-stream” so that whenever new data is available, the server will update that data for the client.
The body() method defines the data returned to the client. In this method, we also define the all() method to generate student information after a second time as follows:
1 2 3 4 5 6 7 8 |
private Flux<Student> all() { RandomStringGenerator rsg = new RandomStringGenerator.Builder() .withinRange('a', 'z') .build(); return Flux.generate((SynchronousSink<Student> sink) -> sink.next(new Student(rsg.generate(10)))) .delayElements(Duration.ofSeconds(1L)); } |
With the RandomStringGenerator object used from the Apache Commons Text library:
1 2 3 4 5 |
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.1</version> </dependency> |
- RouterFunction is also a Functional Interface and it maps the request URL to a HandlerFunction that uses the RequestPredicate object to process the request from the user.
The contents of the RouterFunction interface are as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
/* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.web.reactive.function.server; import reactor.core.publisher.Mono; /** * Represents a function that routes to a {@linkplain HandlerFunction handler function}. * * @author Arjen Poutsma * @since 5.0 * @param <T> the type of the {@linkplain HandlerFunction handler function} to route to * @see RouterFunctions */ @FunctionalInterface public interface RouterFunction<T extends ServerResponse> { /** * Return the {@linkplain HandlerFunction handler function} that matches the given request. * @param request the request to route * @return an {@code Mono} describing the {@code HandlerFunction} that matches this request, * or an empty {@code Mono} if there is no match */ Mono<HandlerFunction<T>> route(ServerRequest request); /** * Return a composed routing function that first invokes this function, * and then invokes the {@code other} function (of the same response type {@code T}) * if this route had {@linkplain Mono#empty() no result}. * @param other the function of type {@code T} to apply when this function has no result * @return a composed function that first routes with this function and then the * {@code other} function if this function has no result * @see #andOther(RouterFunction) */ default RouterFunction<T> and(RouterFunction<T> other) { return new RouterFunctions.SameComposedRouterFunction<>(this, other); } /** * Return a composed routing function that first invokes this function, * and then invokes the {@code other} function (of a different response type) if this route had * {@linkplain Mono#empty() no result}. * @param other the function to apply when this function has no result * @return a composed function that first routes with this function and then the * {@code other} function if this function has no result * @see #and(RouterFunction) */ default RouterFunction<?> andOther(RouterFunction<?> other) { return new RouterFunctions.DifferentComposedRouterFunction(this, other); } /** * Return a composed routing function that routes to the given handler function if this * route does not match and the given request predicate applies. This method is a convenient * combination of {@link #and(RouterFunction)} and * {@link RouterFunctions#route(RequestPredicate, HandlerFunction)}. * @param predicate the predicate to test if this route does not match * @param handlerFunction the handler function to route to if this route does not match and * the predicate applies * @return a composed function that route to {@code handlerFunction} if this route does not * match and if {@code predicate} applies */ default RouterFunction<T> andRoute(RequestPredicate predicate, HandlerFunction<T> handlerFunction) { return and(RouterFunctions.route(predicate, handlerFunction)); } /** * Return a composed routing function that routes to the given router function if this * route does not match and the given request predicate applies. This method is a convenient * combination of {@link #and(RouterFunction)} and * {@link RouterFunctions#nest(RequestPredicate, RouterFunction)}. * @param predicate the predicate to test if this route does not match * @param routerFunction the router function to route to if this route does not match and * the predicate applies * @return a composed function that route to {@code routerFunction} if this route does not * match and if {@code predicate} applies */ default RouterFunction<T> andNest(RequestPredicate predicate, RouterFunction<T> routerFunction) { return and(RouterFunctions.nest(predicate, routerFunction)); } /** * Filter all {@linkplain HandlerFunction handler functions} routed by this function with the given * {@linkplain HandlerFilterFunction filter function}. * @param <S> the filter return type * @param filterFunction the filter to apply * @return the filtered routing function */ default <S extends ServerResponse> RouterFunction<S> filter(HandlerFilterFunction<T, S> filterFunction) { return new RouterFunctions.FilteredRouterFunction<>(this, filterFunction); } /** * Accept the given visitor. Default implementation calls * {@link RouterFunctions.Visitor#unknown(RouterFunction)}; composed {@code RouterFunction} * implementations are expected to call {@code accept} for all components that make up this * router function * @param visitor the visitor to accept */ default void accept(RouterFunctions.Visitor visitor) { visitor.unknown(this); } } |
We can use the static methods of the RequestPredicates object to create a RequestPredicate object:
1 |
RequestPredicate predicate = RequestPredicates.GET("/students"); |
and the static methods of the RouterFunctions object to create the RouterFunction object from the RequestPredicate and HandlerFunction objects above:
1 |
RouterFunction<ServerResponse> routerFunction = RouterFunctions.route(predicate, handlerFunction); |
The entire code for creating the request URL is as follows:
1 2 3 |
HandlerFunction<ServerResponse> handlerFunction = (request) -> ServerResponse.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(all(), Student.class); RequestPredicate predicate = RequestPredicates.GET("/students"); RouterFunction<ServerResponse> routerFunction = RouterFunctions.route(predicate, handlerFunction); |
If using Lambda Expression, the code can be written as follows:
1 |
RouterFunctions.route(RequestPredicates.GET("/students"), (request) -> ServerResponse.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(all(), Student.class)); |
To register this URL request with Spring WebFux, we need to declare the RouterFunction in the Spring container.
For simplicity, we will declare the bean for the RouterFunction object in the SpringWebfluxFunctionalApplication file. The contents of this file will now look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package com.huongdanjava.springwebfluxfunctional; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; import static org.springframework.web.reactive.function.server.RouterFunctions.route; import java.time.Duration; import org.apache.commons.text.RandomStringGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.server.*; import reactor.core.publisher.Flux; import reactor.core.publisher.SynchronousSink; @SpringBootApplication public class SpringWebfluxFunctionalApplication { public static void main(String[] args) { SpringApplication.run(SpringWebfluxFunctionalApplication.class, args); } @Bean public RouterFunction<ServerResponse> routerFunction() { return route(GET("/students"), (request) -> ServerResponse.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(all(), Student.class)); } private Flux<Student> all() { RandomStringGenerator rsg = new RandomStringGenerator.Builder() .withinRange('a', 'z') .build(); return Flux.generate((SynchronousSink<Student> sink) -> sink.next(new Student(rsg.generate(10)))) .delayElements(Duration.ofSeconds(1L)); } } |
That is it, let’s try to run it.
You will also see a new student added in a second and returned to the user as follows: