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>
支持返回 Single
、Flowable
等类型,完美融入响应式流。
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>
✅ 优势:可以直接返回 Mono
或 Flux
,无缝接入 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