Home > Blockchain >  Can I invoke a Kotlin coroutine from Java reflectively and wrap it in CompletableFuture?
Can I invoke a Kotlin coroutine from Java reflectively and wrap it in CompletableFuture?

Time:12-29

I'm reflectively accessing a Kotlin class and trying to invoke a coroutine, wrapping it in a CompletableFuture. Looking at an example from Spring, they manage to wrap such a reflective call in a Mono using kotlinx-coroutines-reactor:

KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
Mono<Object> mono = MonoKt.mono(Dispatchers.getUnconfined(), (scope, continuation) ->
                        KCallables.callSuspend(function, getSuspendedFunctionArgs(target, args), continuation));

I could easily just call Mono#toFuture() but I'm trying to avoid dragging in a dependency to Reactor. Can I achieve the same without going through Mono?

Looking at kotlinx-coroutines-jdk8, there seems to exist something similar to the above, but I can't figure out how to use it. Perhaps:

CompletableFuture<Object> future = FutureKt.future(
         GlobalScope.INSTANCE, Dispatchers.getUnconfined(), CoroutineStart.DEFAULT, (scope, continuation) ->
                KCallables.callSuspend(function, getSuspendedFunctionArgs(target, args), continuation));

But this is just a blind guess, as I have next to 0 Kotlin knowledge. Are CoroutineStart.DEFAULT or GlobalScope.INSTANCE even ok here?

Btw, how is Spring getting away with passing a CoroutineDispatcher (Dispatchers.getUnconfined()) where a CoroutineContext is expected? Should I be using EmptyCoroutineContext instead?

CodePudding user response:

First of all, suspendable code is not easily callable from Java and this is by design. We can do it, but as you already noticed, this is not straightforward and requires some internal knowledge about coroutines.

Usually, it is a good idea to create an adaptation layer to translate suspendable code into something Java understands: CompletableFuture using future() or blocking code using runBlocking(). It is easier to write this in the Kotlin itself, so even if you consume Kotlin library from Java and the library doesn't provide such adapters to futures, you can consider adding a very thin Kotlin wrapper on the consumer side to do the translation.

If this is not possible, we can still use the same future() utility from Java, which you already do. This approach is a little more awkward as we need to e.g. provide values for all optional parameters.

future() is generally pretty easy to use. We need to provide a lambda that will invoke our suspendable function, passing a continuation we received in the lambda. That's it. However, there are multiple parameters to control the process and they complicate things. Also, we don't have to use ReflectJvmMapping.getKotlinFunction(), KCallables.callSuspend() and getSuspendedFunctionArgs() if we don't want to. Again, all we need to do is to invoke the function in the lambda and we can do this "the Java way".

Simple example:

object Database {
    @JvmStatic
    suspend fun loadUsername(userId: Int): String {
        delay(5000)
        return "User$userId"
    }
}
// Note the method receives additional `Continuation` parameter - it is always the last param.
Method method = Database.class.getMethod("loadUsername", int.class, Continuation.class);
CompletableFuture<String> future = FutureKt.future(
        GlobalScope.INSTANCE, EmptyCoroutineContext.INSTANCE, CoroutineStart.DEFAULT, (scope, continuation) -> {
            try {
                // We invoke the method passing its own arguments and the continuation.
                return method.invoke(null, 5, continuation);
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        });

System.out.println("Launched, waiting...");
var result = future.get();
// Result: User5
System.out.println("Result: "   result);

We don't have to use Java reflection here, we can invoke Database.loadUsername(5, continuation) directly, but the question was specifically about the reflection.

Few words about params:

  • GlobalScope.INSTANCE - this is related to the concept of structured concurrency and using GlobalScope means the background operation doesn't have any "owner" to control it. In Java we don't have structured concurrency, so in most cases it is fine to simply use GlobalScope.
  • EmptyCoroutineContext.INSTANCE - I think it is safer to use EmptyCoroutineContext than Dispatchers.getUnconfined(). The difference is that unconfined dispatcher runs a coroutine inside the thread which launched or resumed it; and the empty context in this case uses a special global thread pool. Unconfined is better for performance, but it is a more advanced feature and we should use it only if we know the suspendable code and can decide it will be safe to use unconfined. It doesn't sound good for a generic adaptation layer. I'm not sure what was the reason Spring team decided to use unconfined, maybe they had a more specific use case.

BTW, I also observe Dispatchers.getUnconfined() can't be used as the coroutine context, but I can't explain why. Seems like some kind of a bug in IntelliJ (?). CoroutineDispatcher extends AbstractCoroutineContextElement which implements CoroutineContext.Element which extends CoroutineContext. Therefore, CoroutineDispatcher is a subtype of CoroutineContext. As a matter of fact, we can compile this code and it runs just fine, even if IntelliJ says it is wrong.

  • Related