在 Kotlin 协程中,异常处理是确保程序健壮性的关键。
以下是 5 种核心异常处理方式及其示例代码,涵盖不同场景和最佳实践。
1, 使用try catch捕获异常
fun main() {
// 自定义异常处理器
// exceptionHandler 是一个 CoroutineExceptionHandler,用于处理未在协程内部捕获的异常。
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("全局异常处理器捕获异常: ${exception.message}")
}
// 自定义协程作用域
// customScope 结合了 Dispatchers.Default 调度器和 exceptionHandler,确保协程在默认调度器上执行,并能处理未捕获的异常。
val customScope = CoroutineScope(Dispatchers.Default + exceptionHandler)
// 使用 runBlocking 作为主协程作用域,在其中启动子协程。这样可以保证在 runBlocking 结束时,所有子协程也会结束。
runBlocking {
customScope.launch {
// 在协程内部使用 try-catch 捕获并处理异常,同时如果有未捕获的异常,会被 exceptionHandler 捕获。
try {
delay(100)
throw ArithmeticException("计算错误")
} catch (e: Exception) {
println("协程内捕获异常: ${e.message}")
}
}
// 等待协程执行完毕
delay(200)
// 取消协程作用域,释放资源
customScope.cancel()
}
}
上面的代码输出如下:
协程内捕获异常: 计算错误
2, 使用 CoroutineExceptionHandler 全局处理,适用于 根协程(顶层协程) 的未捕获异常处理。
fun main() {
// 示例:定义全局异常处理器
// CoroutineExceptionHandler:这是 Kotlin 协程库提供的一个接口,用于处理协程中未被捕获的异常。
// 它接受一个 Lambda 表达式作为参数,该 Lambda 表达式有两个参数,
// 第一个参数是 CoroutineContext,代表协程的上下文,这里使用 _ 表示忽略该参数。
// 第二个参数是 Throwable,表示抛出的异常对象。
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("全局捕获: ${throwable.message}")
}
runBlocking {
// launch(exceptionHandler):将之前定义的 exceptionHandler 作为参数传递给 launch 函数,
// 这样该协程中未被捕获的异常就会由这个异常处理器来处理。
// 在构建协程上下文时,采用 SupervisorJob 来管理协程的生命周期。
// SupervisorJob 的特点是,当一个子协程出现异常时,不会影响其他子协程的执行。
// SupervisorJob 与普通的 Job 不同,它不会让一个子协程的异常影响其他子协程。这在需要独立处理每个子协程异常的场景中非常有用。
val job = launch(Dispatchers.Default + SupervisorJob() + exceptionHandler) { // 附加处理器
throw NullPointerException("空指针异常")
}
// join 是 Job 接口的一个方法,用于等待协程执行完毕。调用 job.join() 会阻塞当前协程,直到 job 所代表的协程执行结束。
job.join()
}
}
上面的代码在构建协程上下文时,采用 SupervisorJob 来管理协程的生命周期,就可以正确地捕获异常,输出如下:
全局捕获: 空指针异常
3, 使用 coroutineScope 和 onCompletion 回调
coroutineScope 用于启动多个协程并等待它们完成,而 onCompletion 回调可以在协程完成后(无论正常完成还是异常终止)执行某些操作。
fun main() {
runBlocking {
// coroutineScope 是一个挂起函数,它会创建一个新的协程作用域,该作用域会等待其内部所有子协程完成后才会结束。
// 这体现了结构化并发的思想,有助于管理协程的生命周期和资源。
coroutineScope {
val job = launch {
delay(1000L)
throw Exception("Something went wrong in coroutineScope!")
}
// 为协程设置一个完成回调,当协程完成(正常完成或异常完成)时会触发该回调。
job.invokeOnCompletion { cause ->
// 如果 cause 不为 null,表示协程异常完成,会打印异常信息;否则,表示协程正常完成,会打印相应信息。
if (cause != null) {
println("Coroutine completed exceptionally: ${cause.message}")
} else {
println("Coroutine completed normally")
}
}
}
}
}
上面代码的输出如下:
Coroutine completed exceptionally: Something went wrong in coroutineScope!
Exception in thread "main" java.lang.Exception: Something went wrong in coroutineScope!
当协程抛出异常时,coroutineScope 会捕获这个异常,并且协程的 invokeOnCompletion 回调会被触发,因为 coroutineScope 内的异常不会阻止回调的执行,所以能正常输出异常信息。
当在 launch 后面加上 Dispatchers.Default + SupervisorJob() 后,SupervisorJob 的特性是它不会让一个子协程的异常影响其他子协程,并且异常不会向上传播到父协程作用域(这里是 coroutineScope)。当子协程抛出异常时,SupervisorJob 会取消该子协程,但不会取消整个 coroutineScope。
然而,runBlocking 本身没有对 coroutineScope 内的子协程异常进行捕获和处理。当子协程抛出异常时,这个异常没有被正确捕获,可能会导致程序崩溃或者输出不符合预期。而且,由于 runBlocking 没有处理异常,它可能会提前结束,从而使得 invokeOnCompletion 回调没有机会执行。
相应的解决方案是:在 runBlocking 中添加异常处理逻辑,确保能捕获并处理子协程抛出的异常
fun main() {
runBlocking {
try {
coroutineScope {
val job = launch {
delay(1000L)
throw Exception("Something went wrong in coroutineScope!")
}
job.invokeOnCompletion { cause ->
if (cause != null) {
println("Coroutine completed exceptionally: ${cause.message}")
} else {
println("Coroutine completed normally")
}
}
}
} catch (e: Exception) {
println("Caught exception in runBlocking: ${e.message}")
}
}
}
上面的代码中使用 try-catch 块包裹 coroutineScope,当子协程抛出异常时,runBlocking 能够捕获这个异常并进行处理,同时 invokeOnCompletion 回调也能正常执行,输出协程的完成情况。
上面的代码输出如下:
Coroutine completed exceptionally: Something went wrong in coroutineScope!
Caught exception in runBlocking: Something went wrong in coroutineScope!
4,使用 supervisorScope
supervisorScope 与 coroutineScope 类似,但它们在处理子协程异常时有所不同。在 supervisorScope 中,一个子协程的异常不会导致其他子协程被取消。
fun main() {
runBlocking {
// supervisorScope 是一个挂起函数,它会创建一个新的协程作用域,并且使用 SupervisorJob 来管理该作用域内的协程。
// SupervisorJob 的特点是,当一个子协程抛出异常时,不会影响其他子协程的执行,只会取消抛出异常的子协程本身。
supervisorScope {
val job1 = launch {
delay(1000L)
println("Job 1 completed")
}
val job2 = launch {
delay(500L)
throw Exception("Exception in Job 2")
}
job1.join()
job2.join() // 这行代码实际上不会被执行,因为 job2 已经抛出异常,但它不会影响 job1
}
}
}
上面的代码输出如下:
Exception in thread "main" java.lang.Exception: Exception in Job 2
...
Job 1 completed
从上面的输出可以看出,job2的异常不会影响job1.
5, 处理 async 协程的异常
async 返回的 Deferred 对象需在 await() 时处理异常。
fun main() {
runBlocking {
val deferred = async {
delay(100)
throw IllegalStateException("异步任务失败")
}
try {
deferred.await()
} catch (e: Exception) {
println("异步异常: ${e.message}") // 输出:异步异常: 异步任务失败
}
}
}
上面的代码输出如下:
异步异常: 异步任务失败
Exception in thread "main" java.lang.IllegalStateException: 异步任务失败