1. Overview
In this tutorial, we demonstrate how to use Spring WebFlux module using Kotlin programming language.
We illustrate how to use the annotation-based and the lambda-based style approaches in defining the endpoints.
2. Spring WebFlux and Kotlin
The release of Spring 5 has introduced two new big features among which are native support of the reactive programming paradigm and a possibility to use the Kotlin programming language.
Throughout this tutorial, we assume that we have already configured the environment (consult one of our tutorials on this issue) and understand the Kotlin language syntax (another tutorial on the topic).
3. Annotation-Based Approach
WebFlux allows us to define the endpoints that should handle the incoming requests in the well-known fashion using the SpringMVC framework annotations like @RequestMapping or @PathVariable or convenience annotations like @RestController or @GetMapping.
Despite the fact that the annotation names are the same, the WebFlux’s ones make the methods be non-blocking.
For example, this endpoint:
@GetMapping(path = ["/numbers"], produces = [MediaType.APPLICATION_STREAM_JSON_VALUE]) @ResponseBody fun getNumbers() = Flux.range(1, 100)
produces a stream of some first integers. If the server runs on localhost:8080, then connecting to it by means of the command:
curl localhost:8080/stream
will print out the requested numbers.
4. Lambda-based Approach
A newer approach in defining the endpoints is by means of lambda expressions that are present in Java since the version 1.8. With the help of Kotlin, lambda expressions can be used even with earlier Java versions.
In WebFlux, the router functions are functions determined by a RequestPredicate (in other words, who should manage the request) and a HandlerFunction (in other words, how the request should be elaborated).
A handler function accepts a ServerRequest instance and produces a Mono<ServerResponse> one.
In Kotlin, if the last function argument is a lambda expression, it can be placed outside the parentheses.
Such a syntax allows us to highlight the splitting between the request predicate and the handler function
router { GET("/route") { _ -> ServerResponse.ok().body(fromObject(arrayOf(1, 2, 3))) } }
for router functions.
The function has a clear human-readable format: once a request of type GET arrives at /route, then construct a response (ignoring the content of the request – hence the symbol underscore) with the HTTP status OK and with a body constructed from the given object.
Now, in order to make it work in WebFlux, we should place the router function in a class:
@Configuration class SimpleRoute { @Bean fun route() = router { GET("/route") { _ -> ServerResponse.ok().body(fromObject(arrayOf(1, 2, 3))) } } }
Often, the logic of our applications requires that we should construct more sophisticated router functions.
In WebFlux, Kotlin’s router function DSL defines a variety of functions like accept, and, or, nest, invoke, GET, POST by means of extension functions that allows us to construct composite router functions:
router { accept(TEXT_HTML).nest { (GET("/device/") or GET("/devices/")).invoke(handler::getAllDevices) } }
The handler variable should be an instance of a class implementing a method getAllDevices() with the standard HandlerFunction signature:
fun getAllDevices(request: ServerRequest): Mono<ServerResponse>
as we have mentioned above.
In order to maintain a proper separation of concerns, we may place the definitions of non-related router functions in separate classes:
@Configuration class HomeSensorsRouters(private val handler: HomeSensorsHandler) { @Bean fun roomsRouter() = router { (accept(TEXT_HTML) and "/room").nest { GET("/light", handler::getLightReading) POST("/light", handler::setLight) } } // eventual other router function definitions }
We may access the path variables by means of the String-valued method pathVariable():
val id = request.pathVariable("id")
while the access to the body of ServerRequest is achieved by means of the methods bodyToMono and bodyToFlux, i.e.:
val device: Mono<Device> = request .bodyToMono(Device::class.java)
5. Testing
In order to test the router functions, we should build a WebTestClient instance the routers SimpleRoute().route() that we want to test:
var client = WebTestClient.bindToRouterFunction(SimpleRoute().route()).build()
Now we are ready to test whether the router’s handler function returns status OK:
client.get() .uri("/route") .exchange() .expectStatus() .isOk
The WebTestClient interface defines the methods that allow us to test all HTTP request methods like GET, POST and PUT even without running the server.
In order to test the content of the response body, we may want to use the json() method:
client.get() .uri("/route") .exchange() .expectBody() .json("[1, 2, 3]")
6. Conclusion
In this article, we demonstrated how to use the basic features of the WebFlux framework using Kotlin.
We briefly mentioned the well-known annotation-based approach in defining the endpoints and dedicated more time to illustrate how to define them using router functions in a lambda-based style approach.
All code snippets may be found in our repository over on GitHub.