Getting started with Spring and Coroutines - Part 3

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.

Did you find this article valuable?

Support Ronny Bräunlich by becoming a sponsor. Any amount is appreciated!