1. Introduction

In this tutorial, we’re going to take a look at a functional way to define our endpoints in spring-webmvc and spring-webflux. Since functional endpoints are most useful and concise when working with Kotlin DSL, we’ll present our examples in Kotlin Language.

However, as we’ll see later, there is no requirement to use Kotlin, in general, a similar functional approach is feasible in Java.

2. Dependencies

Let’s start with dependencies that we would need to work. The most recent versions can be found on Maven Central. In either case, we need the Kotlin standard library; that should be obvious. Now, specifically, in case we want to work with spring-webmvc, we would need spring-boot-starter-web:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.3.4</version>
</dependency>

We’re using the starter here instead of the single spring-webmvc dependency to get the spring-boot capabilities from the transitive spring-boot-starter. Also, in case we choose to work with spring-webflux, we need the spring-boot-starter-webflux and a couple of other dependencies in order to work with Kotlin coroutines:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>3.3.4</version>
</dependency>
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-core</artifactId>
    <version>1.9.0</version>
</dependency>
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-reactor</artifactId>
    <version>1.9.0</version>
</dependency>

Now, having dependencies in place, we can finally begin. But first, we need to understand how functional bean definitions even work in Spring in general.

3.1. Functional Beans Registration

There are a lot of possible ways to create beans in spring-framework. The most famous examples are XML bean declarations, Java Config, and configuration via annotations. But there is another way to register beans in spring – a functional way. Let’s take a look at an example of that:

@SpringBootApplication
open class Application

fun main(vararg args: String) {
    runApplication(*args) {
        addInitializers(
            beans {
                bean(
                  name = "functionallyDeclaredBean",
                  scope = BeanDefinitionDsl.Scope.SINGLETON,
                  isLazyInit = false,
                  isPrimary = false,
                  function = {
                      BigDecimal(1.0)
                  }
                )
            }
        )
    }
}

Here, we’re launching the Spring Boot application via the runApplication() function, and setting the configuration source as the Application class. This function actually accepts two parameters – the Java process arguments (which are args) and function, with SpringApplication as receiver (The second argument is passed out of parenthesis). Let’s first briefly explain how the code above works.

Because the last parameter is of the function type, we can move it out of parenthesis, which we did in our example.  The fact that runApplication() has a function parameter with specific receiver is very important here. It is because of this receiver (SpringApplication in our case), we can use the addInitializers() function inside the past function body. Also, functions with receiver are actually the crucial feature in Kotlin that allows not only for such concise bean registration as we saw above but also it allows for type-safe DSL builders in Kotlin in general.

It is because of these two features in Kotlin – functions with receiver and passage of functions outside parenthesis such DSL as above is possible. Now, let’s continue examining the example.

So, to put things simply – in the code above, there is a chain of inner lambadas with various receivers. These lambdas create a set of BeanDefinition-s, in our case just one bean definition for creation of a bean of BigDecimal type, that would be registered via ApplicationContextInizilizer.

4. Functional Endpoints With MVC

But the code above registers an ordinary bean in context, and it does not relate anyhow to spring-webmvc or spring-webflux. To register an endpoint that would handle HTTP requests, we need to invoke another lambda inside bean DSL function – router:

beans {
    bean {
        router {
            GET("/endpoint/{country}") { it : ServlerRequest ->
                ServerResponse.ok().body(
                  mapOf(
                    "name" to it.param("name"),
                    "age" to it.headers().header("X-age")[0],
                    "country" to it.pathVariable("country")
                  )
                )
            }
        }
    }
}

Let’s review this example in a bit more detail. Although lambdas that have one parameter may access the parameter via it as its name, we’re explicitly specifying the parameter of the router function along with its type for demonstration purposes. The parameter is of the type ServerRequest, which represents an abstraction over the client’s HTTP request. We can then get any information we want from the request to process the request, like in the example above – getting the query parameter, header of the request, or path variable.

This approach is very similar to creating the RestController with one method annotated with @GetMapping:

@RestController
class RegularController {
    @GetMapping(path = ["/endpoint/{country}"])
    fun getPerson(
      @RequestParam name: String,
      @RequestHeader(name = "X-age") age: String,
      @PathVariable country: String
    ): Map {
        return mapOf(
          "name" to name,
          "age" to age,
          "country" to country
        )
    }
}

From a partial perspective, these two approaches are pretty much identical, for instance, HTTP filters for spring security would work in both cases in the same way.

5. Origins of Functional Endpoints

It’s crucial to understand that the router DSL function above is actually just the convenient abstraction over the RouterFunction API. This API exists for both spring-webmvc and spring-webflux modules. That actually means that any code that uses router function DSL can also use RotuerFunction directly:

@Bean
open fun configure() {
    RouterFunctions.route()
      .GET("/endpoint/{country}") {
          ServerResponse.ok().body(
            mapOf(
              "name" to it.param("name"),
              "age" to it.headers().header("X-age")[0],
              "country" to it.pathVariable("country")
            )
          )
      }
    .build()
}

This would be perfectly identical to router DSL function usage. Notice, that we’re not adding any initializers to the context. That is intentional to emphasize that it generally does not matter how we register our RouterFunction beans in the context – via context initializer or Java Config.

6. Spring Webflux Functional Endpoints

As stated, spring-webflux has a functional approach to writing endpoints as well. Similar to spring-webmvc, we can either use the RouterFunction API directly or use router function DSL abstraction. Let’s quickly review the direct usage of a RouterFunction DSL:

@Bean
open fun configure(): RouterFunction {
    return RouterFunctions.route()
      .GET("/users/{id}") {
          ServerResponse
            .ok()
            .body(usersRepository.findUserById(it.pathVariable("id").toLong()))
      }
      .POST("/create") {
          usersRepository.createUsers(it.bodyToMono(User::class.java))
          return@POST ServerResponse
            .ok()
            .build()
      }
      .build()
}

So this is quite similar to spring-webmvc and should be pretty straightforward. The router function DSL analog would look like this:

@Bean
open fun endpoints() = router {
    GET("/users/{id}") {
        ServerResponse
          .ok()
          .body(usersRepository.findUserById(it.pathVariable("id").toLong()))
    }
    POST("/create") {
        usersRepository.createUsers(it.bodyToMono(User::class.java))
        return@POST ServerResponse
          .ok()
          .build()
    }
}

So, this router function approach is more concise in Kotlin, just because we can use it via declarative DSL style. But, as it is clear now, under the hood we can do absolutely the same in Java.

7. Kotlin Coroutines With Functional Endpoints

Lastly, it’s worth mentioning that spring functional endpoints also support Kotlin coroutines. Let’s break down the following use case:

@Bean
open fun registerForCo() =
    coRouter {
        GET("/users/{id}") {
            val customers : Flow<User> = usersRepository.findUserByIdForCoroutines(
              it.pathVariable("id").toLong()
            )
            ServerResponse.ok().bodyAndAwait(customers)
        }
    }

Here, we’re using the coRouter DSL function. This is also an abstraction over RouterFunction API, but this abstraction is built with suspended HandlerFunction. In other words, the callback that we pass into GET is actually the suspended function, that in turn invokes the findUserByIdForCoroutines method, which is also a suspended function.

Notice the return value of the findUserByIdForCoroutines method is Flow. It is important here since the coRouter function DSL is actually the wrapper over the reactive RouterFunction, not the webmvc one. Therefore, because Flow is essentially the asynchronous cold stream that emits values sequentially over some period of time, it makes it generally comparable to reactor’s Publisher. Therefore, under the hood, Spring just makes a conversion from Flow to project reactor’s Publisher and then the workflow is similar to one of WebFlux’s RouterFunction API.

8. Conclusion

In this article, we discussed the functional API for spring-webflux and spring-webmvc. It is based on RouterFunction API, which is itself different in the case of working with WebFlux (i.e. via DispatcherHandler), and in the case of working with WebMVC (i.e. via plain old DispatcherServlet). The RouterFunction can be used directly and there is no problem with that, but when we’re with Kotlin, there is a more concise and elegant way to work with RouterFunction API, that is via router DSL functions. This is nothing more but an abstraction over RouterFunction, made possible by Kotlin extension functions with receiver and higher order functions features. There is also an option to work with coroutines and Kotlin DSL. The coroutines DSL is built over the webflux’s RouterFunction, so to work with it, we need WebFlux.


原始标题:Functional HTTP Endpoints with Spring MVC/WebFlux and Kotlin