You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Retry配合Kotlin协程async调用时重试失效问题

问题描述

注意:下述test函数是通过launch协程发起调用的。
如下代码可正常触发Spring Retry重试逻辑:

@Retryable(value=[RetryException::class])
suspend fun test() {
  throw RetryException("blah")
}

但在函数中新增async异步调用逻辑后,重试机制就不再生效,对应实现代码如下:

@Retryable(value=[RetryException::class])
suspend fun test() {
  val deferred = supervisorScope {
    async { library.method() }
  }
  deferred.await()
  throw RetryException("blah")
}

根本原因

核心问题是Spring Retry的AOP拦截逻辑和Kotlin协程的执行、异常传播机制不兼容:

  • Spring Retry基于动态代理实现,拦截逻辑仅作用于代理方法的初始调用栈,只有方法在初始调用栈帧中直接抛出匹配配置规则的异常时,才会触发重试。
  • 当suspend函数内存在挂起点(示例代码中的supervisorScope、deferred.await()都属于挂起函数)时,函数执行到挂起点会暂停,后续逻辑会在协程调度器分配的线程中通过Continuation恢复执行,这部分恢复执行的代码已经脱离了Spring AOP代理的原始调用栈,后续抛出的RetryException无法被Spring Retry的拦截器捕获,自然不会触发重试。
  • 能正常触发重试的示例代码中,test()方法进入后直接抛出异常,没有触发任何挂起操作,异常始终在代理的原始调用栈中抛出,因此可以被正常识别。
  • 额外影响点:supervisorScope的异常隔离逻辑会包装子协程抛出的异常,就算异常在挂起前抛出,包装后的异常类型也会和配置的RetryException不匹配,同样会导致重试失效。

解决方案

按落地成本从低到高排序:

  • 方案1:调整代码执行顺序
    如果异常判断逻辑不需要依赖library.method()的执行结果,直接把抛出RetryException的逻辑移到所有挂起点之前,保证异常在代理原始调用栈中抛出即可。
  • 方案2:升级Spring Retry版本+手动解包异常
    将Spring Retry升级到1.3.4及以上版本(该版本新增了对Kotlin挂起函数的初步支持),同时在代码中手动剥离协程包装的异常外壳,抛出原始匹配的异常:
    // 异常解包扩展函数
    fun Throwable.unwrap(): Throwable {
        var current = this
        // 递归剥离协程包装的异常类型
        while (current is CancellationException || current is CompletionException) {
            current = current.cause ?: break
        }
        return current
    }
    
    @Retryable(value=[RetryException::class])
    suspend fun test() {
        val result = try {
            supervisorScope {
                val deferred = async { library.method() }
                deferred.await()
            }
        } catch (e: Exception) {
            throw e.unwrap()
        }
        // 挂起逻辑执行完后,抛出的异常也要保证是未被包装的原始类型
        if (!result.isSuccess) {
            throw RetryException("blah")
        }
    }
    
  • 方案3:替换为协程原生重试组件(推荐)
    直接使用专门为Kotlin协程设计的重试组件,这类组件不依赖AOP代理,原生支持协程上下文传播、挂起函数执行逻辑,不会出现调用栈脱离、异常包装导致的重试失效问题,是协程场景下的最优解。

内容的提问来源于stack exchange,提问作者Nayan Seth

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 02:30:59