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 |
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:
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)
}
}
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.