Getting started with Spring and Coroutines - Part 3
Things that don't work (Annotation magic)
Even Coroutines have to be taken with a grain of salt. This article is about the things that will not work when you go reactive in Spring. Though Spring is trying to support Kotlin features as good as possible it still lacks some.
Initially, I was planning to give you some specific examples of things that do not work. Like this annotation or that one. But while doing my research I came to a different conclusion. It is not about specifics that do not work well with Coroutines. I came to a general conclusion:
Expect that anything that uses annotation magic does not work
There might be exceptions, since I am pretty sure that I do not know every single annotation, but regarding the ones I use most often, nothing works with Coroutines out of the box.
Let's take a look at some examples.
Cacheable
If you want to annotate a suspend function with @Cacheable
you will be quite disappointed. Due to the fact that the Kotlin compiler will modify the method signature and add a Continuation parameter. Basically, the Continuation
will mess with the caching interceptor. There is a pretty good SoF answer regarding this.
Workaround
The general solution for all incompatibilities presented here is to go back to the basics, i.e., we code manually what the annotations do for us automagically. In the case of the @Cacheable
annotation we must use the CacheManager
ourselves. E.g., when using Caffeine, we need a CacheManager
bean:
@Bean
fun cacheManager(caffeine: Caffeine<Any, Any>): CacheManager =
CaffeineCacheManager().apply {
setCaffeine(Caffeine.newBuilder().maximumSize(10_000))
}
And then we can use the Bean to create Caches when needed:
private val cache: Cache = cacheManager.getCache("cacheName")!!
Lastly, we check presence and insert into the cache when necessary:
return if (cache[key] != null) {
cache.get(key, MyClass::class.java)
} else {
...
cache.put(key, myClass)
}
Additionally, as you might have read in the SoF answer, you could also fall back to using a Deferred
as return value. But using a Deferred
here might bring other problems since we are no longer invoking a method but rather starting a job.
Transactional
Currently, if you do not happen to use the R2DB driver, there is no ReactiveTransactionManager
in your application autoconfigured. This means that reactive methods, like suspend functions, cannot be used with the @Transactional
annotation.
As far as I know, it is problematic to stretch a transaction across several threads. And in case of suspend functions, you might switch the thread several times.
Workaround
If you want to stick to suspend functions, you will have to use the TransactionTemplate
. You should be able to inject an org.springframework.transaction.PlatformTransactionManager
and then create a TransactionTemplate
:
val transactionTemplate = TransactionTemplate(platformTransactionManager)
You can then call the executeWithoutResult
or execute
methods. But be careful. I did not test this. You should avoid calling other suspend functions without runBlocking
. The way I see it, everything you do within one of those two execute functions should be non-reactive due to the reason mentioned above.
CircuitBreaker
As you can see from this issue (effective 11.10.2022) there is no support for the @CircuitBreaker
annotation. Nevertheless, Resilience4J supports Kotlin Coroutines.
Workaround
As you can read in the documentation you may execute or decorate the suspend functions. Something like this should work:
private suspend fun <T> makeCall(yourLambda: suspend () -> T): T =
circuitBreaker.executeSuspendFunction {
timeLimiter.executeSuspendFunction(yourLambda)
}
FeignClient
If you are a fan of the declarative FeignClient I have to disappoint you, too. There is currently no support for reactive methods.
Workaround
There is a community project called "Feign Reactive" which you can find here. It allows you to use Mono
or Flux
as return types. You could then transform those into Coroutines by using the extension methods from kotlinx-coroutines-reactor
like await()
.
Bean
Not even the @Bean
annotation works with suspend functions. Somehow, the inserted Continuation
also messes with Spring's ConstructorResolver
.
Okay, this is quite an artificial example since there should be no suspend functions involved when creating a bean. Still, it shows that there are some problems left to be solved.
Final remarks
As always with new technologies the surrounding ecosystem must catch up. Though, Kotlin and its Coroutines aren't that new, a framework as big a Spring cannot change quickly.
There was a project that tried to overcome the limitations but it hasn't received any updates in a while (github.com/konrad-kaminski/spring-kotlin-co..). So, I suppose it is EOL.
Personally, I hope that with Spring 6 and Spring Boot 3 there will be better support out-of-the-box for all the things I have shown in this article. We will know more at the end of the year.