/ SPRING BOOT, DSL, FUNCTIONAL, REACTIVE

Spring Boot, migrating to functional

In the latest years, there has been some push-back against frameworks, and more specifically annotations: some call it magic, and claim it’s hard to understand the flow of the application. It’s IMHO part of a developer’s job to know about some main frameworks. But it’s hard to argue in favor of annotations when there are more annotations than actual code.

As usual in programming, nothing is black and white. Some specific usages are more than relevant, especially those related to documentation. One that naturally springs to mind is the @Transactional annotation: not only does it make the annotated method transactional, it marks it as such in the documentation.

Spring and Spring Boot latest versions go along this trend, by offering an additional way to configure beans with explicit code instead of annotations. It’s called functional, because it moves from objects to behavior.

This post aims to describe a step-by-step process to achieve that.

Reactive all the way down

Let’s consider a standard REST CRUD application, on top of a SQL database. The first issue when migrating to functional configuration is that the current API allows that flavor only for reactive.

Keep in minde that reactive is the way to go only for specific use-cases (just like micro-services, mono repos, etc.). Though Spring makes it very easy, please don’t migrate to reactive just "because you can".

Switching to reactive involves some non-trivial steps:

  • There’s no reactive JDBC API available yet. However, there’s a MongoDB one. Hence, the SQL database needs to be replaced with a MongoDB instance.
  • As a corollary, entities needs to be annotated with @Document instead of @Entity
  • Also, initialization scripts (e.g. import.sql) have to be migrated to programmatic initialization
  • Change all single return type to Mono<X>
  • Change all collections return type to Flux<X>

At this point, one can wonder if it’s still the same application…​ In case the answer doesn’t bother you too much, let’s go further.

Kotlin 4TW

While Kotlin is not strictly necessary, it’s a huge help in migrating to functional configuration because of the Kotlin Bean DSL. Besides, all snippets below will be in Kotlin anyway.

Controllers to routes

Here’s a pretty straightforward REST controller:

@RestController
class PersonController(private val personRepository: PersonRepository) {

    @GetMapping("/person")
    fun readAll() = personRepository.findAll()

    @GetMapping("/person/{id}")
    fun readOne(@PathVariable id: Long) = personRepository.findById(id)
}

Obviously, Spring controllers are sprinkled with annotations.

Spring WebFlux introduces the concept of route: a route is an association between a path and some logic. The first step for the migration would be to move from controllers to routes.

The relevant class diagram looks like the following:

routerfunction

Using this API, the above controller can be rewritten like this:

@Bean
fun routes(repository: PersonRepository) = nest(
  path("/person"),
    route(
      GET("/{id}"),
        HandlerFunction {
          ServerResponse.ok()
                        .body(repository.findById(it.pathVariable("id").toLong()))
        }
    ).andRoute(
      method(HttpMethod.GET),
        HandlerFunction { ServerResponse.ok().body(repository.findAll()) }
    )
)

The logic is wrapped inside a HandlerFunction functional interface - this is possible because Java 8 (and Kotlin) treats functions as first-class citizens.

Details regarding its implementation can be found in the documentation.

It’s also possible to move the HandlerFunction implementations out of the function into a dedicated class:

class PersonHandler(private val personRepository: PersonRepository) {
  fun readAll(request: ServerRequest) =
    ServerResponse.ok().body(personRepository.findAll())
  fun readOne(request: ServerRequest) =
    ServerResponse.ok().body(
      personRepository.findById(request.pathVariable("id").toLong())
    )
}

@Bean
fun routes(handler: PersonHandler) = nest(
  path("/person"),
    route(GET("/{id}"), HandlerFunction(handler::readOne))
    .andRoute(method(HttpMethod.GET), HandlerFunction(handler::readAll))
)
The only reason for the PersonHandler class is to hold the repository dependency. Without any dependency, functions could be moved to top-level.

Bean annotations to functional

After having migrated controllers, the next step is to remove @Bean annotation from bean-returning methods. As an example, this is the data initialization method:

@SpringBootApplication
class MigrationDemoApplication {

  @Bean
  fun initialize(repository: PersonRepository) = CommandLineRunner {
    repository.insert(
      arrayListOf(Person(1, "John", "Doe", LocalDate.of(1970, 1, 1)),
                  Person(2, "Jane", "Doe", LocalDate.of(1970, 1, 1)),
                  Person(3, "Brian", "Goetz"))
    ).blockLast(Duration.ofSeconds(2))
  }
}

With the help of the Kotlin bean DSL, migration is easy as pie:

fun beans() = beans {
  bean {
    CommandLineRunner {
      ref<PersonRepository>().insert(
              arrayListOf(Person(1, "John", "Doe", LocalDate.of(1970, 1, 1)),
                          Person(2, "Jane", "Doe", LocalDate.of(1970, 1, 1)),
                          Person(3, "Brian", "Goetz"))
      ).blockLast(Duration.ofSeconds(2))
    }
  }
}

@SpringBootApplication
class MigrationDemoApplication {

  @Autowired
  fun register(ctx: GenericApplicationContext) = beans().initialize(ctx)
}

Just changing from a @Bean function is not enough. The beans wrapped by the return value of the beans() function also need to be programmatically added to the context. This is done via BeansDefinitionDsl.initialize().

Route annotations to functional

The next logical step is to migrate the above routes in the same way. First, let’s move the code above to a dedicated router DSL:

fun routes(handler: PersonHandler) = router {
  "/person".nest {
    GET("/{id}", handler::readOne)
    GET("/", handler::readAll)
  }
}

For the next step, let’s move the function to a class for constructor-injection of the PersonHandler:

class PersonRoutes(private val handler: PersonHandler) {
  fun routes() = router {
    "/person".nest {
      GET("/{id}", handler::readOne)
      GET("/", handler::readAll)
    }
  }
}

Now, it’s possible to get the function and register it:

fun beans() = beans {
  bean {
    PersonRoutes(PersonHandler(ref())).routes()
  }
  ...
}

Unfortunately, this doesn’t work. Routes registration happens too late in the Spring Webflux lifecycle.

This is a work in progress. Expect it to be fixed soon, e.g. Spring Boot 2.1

The workaround is to move the registration from the application class into a dedicated initialization class and configure that class:

@SpringBootApplication
class MigrationDemoApplication

class BeansInitializer: ApplicationContextInitializer<GenericApplicationContext> {

  override fun initialize(context: GenericApplicationContext) {
    beans().initialize(context)
  }
}
application.properties
context.initializer.classes=ch.fr.blog.spring.functionalmigration.BeansInitializer

That kickstart the initialization early enough into the application lifecycle so that routes are now correctly registered.

Functional initialization

Perhaps the application.properties trick still seems too magical?

The final step is to migrate from properties configuration to programmatic configuration.

fun main(args: Array<String>) {
  runApplication<MigrationDemoApplication>(*args) {
    addInitializers(beans())
  }
}

Conclusion

It’s possible to migrate from an annotation-based configuration to one nearly fully functionally-oriented. However, it also requires to migrate the application to reactive. Still, there’s no denying the final result is easier to analyze from a developer’s point of view.

The Spring Fu project is an on-going experimental initiative to go the whole nine yards and do without annotations at all.

Many thanks to Sébastien Deleuze for the proof-reading of this post and his feedback.

The complete source code for this post can be found on Github.
Nicolas Fränkel

Nicolas Fränkel

Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Also double as a trainer and triples as a book author.

Read More
Spring Boot, migrating to functional
Share this