1. 简介

Kotlin 的协程(Coroutines)让异步编程可以像写同步代码一样简洁。其核心是 suspend 函数,这类函数可以在不阻塞线程的前提下暂停和恢复执行。本文将深入探讨如何在 Java 中调用 Kotlin 的挂起函数,并分析其中的注意事项和常见踩坑点。

如果你的项目是 Kotlin 和 Java 混合开发,这种跨语言调用几乎是不可避免的。理解底层机制,能帮你避免很多莫名其妙的 ClassCastException 或线程阻塞问题。

2. 挂起函数与 Java 的交互原理

我们先看一个简单的 Kotlin 挂起函数:

suspend fun checkIn(): String {
    delay(10)
    return "Welcome"
}

当这个函数被编译成 JVM 字节码后,在 Java 世界里它长这样:

public static final Object checkIn(@NotNull Continuation var0) {
    ...
}

✅ 关键点:所有 suspend 函数都会被编译成带 Continuation 参数的方法,这是典型的 CPS(Continuation-Passing Style)风格。

Kotlin 编译器会自动管理 Continuation 对象,并在 suspend 函数执行完毕后,调用其 resume()resumeWithException() 方法来传递结果或异常。这两个方法本质上是对 resumeWith(Result<T>) 的封装:

public inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))

public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
    resumeWith(Result.failure(exception))

⚠️ 注意:BaseContinuationImpl 是 Kotlin 内部实现类,负责处理挂起点的状态机逻辑。你不需要手动实现它,但要明白它的存在意味着——直接在 Java 中模拟完整的协程行为极其复杂且容易出错

3. 在 Java 中实现 Continuation(不推荐)

虽然理论上可行,但在 Java 中手动实现 Continuation 接口是一种“自己造轮子”的高风险操作。主要原因如下:

  • Kotlin 的 Continuation 实现是 internal 的,Java 无法直接访问。
  • 手动状态机管理极易出错,尤其是涉及多个挂起点时。

不过为了理解原理,我们可以实现一个极简版本:

class CustomContinuation<T> implements Continuation<T> {

    @Override
    public void resumeWith(@NotNull Object o) {
        if (o instanceof Result.Failure)
            consumeException((((Result.Failure) o).exception));
        else
            consumeResult((T)o);
    }

    @NotNull
    @Override
    public CoroutineContext getContext() {
        return EmptyCoroutineContext.INSTANCE;
    }
}

这里的 consumeException()consumeResult() 只是占位符,实际业务中你需要在这里处理回调逻辑。

3.1 使用 CustomContinuation 调用挂起函数

更实用的做法是将结果交给 CompletableFuture 管理:

class CustomContinuation<T> implements Continuation<T> {
    private final CompletableFuture<T> future;

    public CustomContinuation(CompletableFuture<T> future) {
        this.future = future;
    }

    @Override
    public void resumeWith(@NotNull Object o) {
        if (o instanceof Result.Failure)
            future.completeExceptionally(((Result.Failure) o).exception);
        else
            future.complete((T) o);
    }
    
    @NotNull
    @Override
    public CoroutineContext getContext() {
        return EmptyCoroutineContext.INSTANCE;
    }
}

然后这样调用:

CompletableFuture<String> suspendResult = new CompletableFuture<>();
checkIn(new CustomContinuation<>(suspendResult));

Assertions.assertEquals("Welcome", suspendResult.get());

❌ 踩坑提醒:这种方式只适用于顶层调用。一旦你的 suspend 函数内部又调用了其他 suspend 函数,这种简单实现就会失效。

3.2 处理异常情况

测试一下异常场景。定义一个会抛异常的 suspend 函数:

suspend fun checkInClosed(): String {
    delay(10)
    throw Exception("Sorry! We are closed.")
}

Java 调用并验证异常:

CompletableFuture<String> suspendResult = new CompletableFuture<>();
checkInClosed(new CustomContinuation<>(suspendResult));

Assertions.assertThrows(Exception.class, suspendResult::get);

这说明我们的 CustomContinuation 能正确传递异常。

4. 使用协程扩展库(推荐方案)

Kotlin 官方提供了 kotlinx-coroutines-* 系列扩展库,专门用于桥接不同异步模型。这才是生产环境应该采用的方式。

4.1 阻塞调用:runBlocking

最简单粗暴的方式——让异步变同步。使用 BuildersKt.runBlocking

String result = BuildersKt.runBlocking(
  EmptyCoroutineContext.INSTANCE,
  (scope, continuation) -> checkIn(continuation)
);
Assertions.assertEquals("Welcome", result);

📌 依赖:

<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-core-jvm</artifactId>
    <version>1.7.3</version>
</dependency>

⚠️ 注意:第一个参数 CoroutineContext 是一个混合了集合与映射特性的结构,用来保存协程的上下文信息(如调度器、Job 等)。EmptyCoroutineContext.INSTANCE 表示使用默认上下文。

4.2 转为 Future

不想阻塞主线程?用 FutureKt.future()

CompletableFuture<String> suspendResult = FutureKt.future(
  CoroutineScopeKt.CoroutineScope(EmptyCoroutineContext.INSTANCE),
  EmptyCoroutineContext.INSTANCE,
  CoroutineStart.DEFAULT,
  (scope, continuation) -> checkIn(continuation)
);
Assertions.assertEquals("Welcome", suspendResult.get());

📌 依赖:

<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-jdk8</artifactId>
    <version>1.7.3</version>
</dependency>

参数说明:

  • CoroutineScope: 协程作用域
  • CoroutineContext: 上下文
  • CoroutineStart: 启动模式(DEFAULT 表示立即执行)
  • Function2: 实际执行的 suspend 逻辑

4.3 与 RxJava 集成

如果你的项目使用 RxJava,可以用对应扩展:

Single<String> suspendResult = RxSingleKt.rxSingle(
  EmptyCoroutineContext.INSTANCE, 
  (scope, continuation) -> checkIn(continuation)
);
Assertions.assertEquals("Welcome", suspendResult.blockingGet());

📌 依赖(RxJava2 示例):

<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-rx2</artifactId>
    <version>1.7.3</version>
</dependency>

支持返回 SingleFlowable 等类型,完美融入响应式流。

4.4 与 Project Reactor 集成

对于 Spring WebFlux 用户,Reactor 扩展是最佳选择:

Mono<String> suspendResult = MonoKt.mono(
  EmptyCoroutineContext.INSTANCE, 
  (scope, continuation) -> checkIn(continuation)
);
Assertions.assertEquals("Welcome", suspendResult.block());

📌 依赖:

<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-reactor</artifactId>
    <version>1.7.3</version>
</dependency>

✅ 优势:可以直接返回 MonoFlux,无缝接入 WebFlux 控制器。

5. 更优雅的方案:在 Kotlin 层做封装

上面所有 Java 调用方式都显得冗长且易错。真正的最佳实践是在 Kotlin 侧就完成转换,暴露对 Java 友好的接口。

例如,在 Kotlin 中封装一个返回 CompletableFuture 的函数:

@DelicateCoroutinesApi
fun checkInAsync(): CompletableFuture<String> = GlobalScope.future { checkIn() }

💡 提示:IntelliJ 会建议添加 @DelicateCoroutinesApi 注解,提醒调用者该 API 使用了全局作用域,需谨慎使用。

Java 侧调用变得极其简洁:

CompletableFuture<String> suspendResult = checkInAsync();
Assertions.assertEquals("Welcome", suspendResult.get());

✅ 这才是混合项目应有的协作方式:Kotlin 负责处理协程复杂性,Java 只消费标准化的异步类型(如 CompletableFuture、Mono 等)。

6. 总结

方案 适用场景 推荐度
手动实现 Continuation 学习原理 ❌ 不推荐
runBlocking 测试/启动初始化 ⚠️ 慎用
Future/Rx/Reactor 扩展 Java 主导调用 ✅ 推荐
Kotlin 封装暴露 混合项目长期维护 ✅✅ 强烈推荐

最终结论:不要让 Java 直接面对 suspend 函数。通过 Kotlin 层的适配器模式,将挂起函数包装成 Java 常见的异步类型,才是可持续的工程实践。

文中所有示例代码已托管至 GitHub:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-concurrency-2


原始标题:Calling Kotlin Suspending Functions from Java