Stacktrace-decoroutinator
用于恢复Kotlin协程中抛出异常的堆栈跟踪的库。
支持JVM 1.8或更高版本,以及Android API 26或更高版本。
动机
协程是Kotlin的一项重要特性,它允许您以同步风格编写异步代码。
它非常完美,直到您需要调查代码中的问题。
常见问题之一是协程中抛出的异常堆栈跟踪被缩短。例如,以下代码打印出下面的堆栈跟踪:
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
suspend fun fun1() {
delay(10)
throw Exception("exception at ${System.currentTimeMillis()}")
}
suspend fun fun2() {
fun1()
delay(10)
}
suspend fun fun3() {
fun2()
delay(10)
}
fun main() {
try {
runBlocking {
fun3()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
java.lang.Exception: exception at 1641842199891
at MainKt.fun1(main.kt:6)
at MainKt$fun1$1.invokeSuspend(main.kt)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at MainKt.main(main.kt:21)
at MainKt.main(main.kt)
堆栈跟踪并不代表真实的协程调用堆栈:缺少了fun3
和fun2
函数的调用。
在复杂系统中,可能会缺失更多的调用。这会使调试变得更加困难。
一些遇到这个问题的例子:
- https://github.com/arrow-kt/arrow/issues/2647
- https://stackoverflow.com/questions/54349418/how-to-recover-the-coroutines-true-call-trace
- https://stackoverflow.com/questions/69226016/how-to-get-full-exception-stacktrace-when-using-await-on-completablefuture
Kotlin团队了解这个问题,并提出了一个解决方案,但它只解决了部分情况。 例如,上面示例中的异常仍然缺少一些调用。
解决方案
Stacktrace-decoroutinator替换了协程唤醒的实现。
它在运行时生成与整个协程调用堆栈匹配的方法名称。
这些方法除了按照协程调用堆栈顺序相互调用外,不执行任何操作。
因此,如果协程抛出异常,它们会在创建异常堆栈跟踪时模拟协程的真实调用堆栈。
JVM
有三种方法可以为JVM启用Stacktrace-decoroutinator。
- 如果您使用Gradle构建项目,只需应用ID为
dev.reformator.stacktracedecoroutinator
的Gradle插件。 - 添加依赖
dev.reformator.stacktracedecoroutinator:stacktrace-decoroutinator-jvm:2.4.1
并调用方法DecoroutinatorRuntime.load()
。 - 在JVM启动参数中添加
-javaagent:stacktrace-decoroutinator-jvm-agent-2.4.1.jar
。相应的依赖是dev.reformator.stacktracedecoroutinator:stacktrace-decoroutinator-jvm-agent:2.4.1
。
第一个选项在构建时生成辅助方法,其他两个在运行时使用Java instrumentation API。
使用示例:
import dev.reformator.stacktracedecoroutinator.runtime.DecoroutinatorRuntime
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
suspend fun rec(depth: Int) {
if (depth == 0) {
yield()
throw Exception("exception at ${System.currentTimeMillis()}")
}
rec(depth - 1)
}
fun main() {
DecoroutinatorRuntime.load() // 启用stacktrace-decoroutinator运行时
try {
runBlocking {
rec(10)
}
} catch (e: Exception) {
e.printStackTrace() // 打印包含10次递归调用的完整堆栈跟踪
}
}
输出:
java.lang.Exception: exception at 1722597709832
at ExampleKt.rec(example.kt:8)
at ExampleKt$rec$1.invokeSuspend(example.kt)
at kotlin.coroutines.jvm.internal.JavaUtilsImpl$1.apply(JavaUtilsImpl.java:52)
at ExampleKt.rec(example.kt:10)
at ExampleKt.rec(example.kt:10)
at ExampleKt.rec(example.kt:10)
at ExampleKt.rec(example.kt:10)
at ExampleKt.rec(example.kt:10)
at ExampleKt.rec(example.kt:10)
at ExampleKt.rec(example.kt:10)
at ExampleKt.rec(example.kt:10)
at ExampleKt.rec(example.kt:10)
at ExampleKt.rec(example.kt:10)
at ExampleKt$main$1.invokeSuspend(example.kt:17)
at dev.reformator.stacktracedecoroutinator.runtime.AwakenerKt.awake(awakener.kt:93)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(basecontinuation.kt:20)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:95)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:69)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:48)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at ExampleKt.main(example.kt:16)
at ExampleKt.main(example.kt)
Android
对于Android,只有一种方法可以启用Stacktrace-decoroutinator - 将Gradle插件dev.reformator.stacketracedecoroutinator
应用到您的应用程序项目中。
plugins {
id("dev.reformator.stacktracedecoroutinator") version "2.4.1"
}
使用ProGuard
如果您使用ProGuard(通常用于Android),请添加以下排除规则:
-keep @kotlin.coroutines.jvm.internal.DebugMetadata class * { *; }
-keep @dev.reformator.stacktracedecoroutinator.runtime.DecoroutinatorTransformed class * { *; }
Jacoco和Decoroutinator的问题
同时使用Jacoco和Decoroutinator作为Java代理可能会导致代码覆盖率损失。这是Jacoco的常见问题。为了不损失覆盖率,请确保Jacoco代理在Decoroutinator代理之前。更多信息请参见 https://github.com/Anamorphosee/stacktrace-decoroutinator/issues/24。
交流
如有任何问题,请随时在讨论区提问。