1. Overview

In this tutorial, we demonstrate how to use the 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 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 requested 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 acceptandornestinvokeGETPOST by means of extension functions that allow 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.

Comments are closed on this article!