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

跨Reactive/协程边界时异常栈跟踪丢失问题求助

Spring WebFlux Kotlin协程中OptimisticLockingFailureException栈跟踪丢失问题

环境

  • Spring Boot 3
  • WebFlux
  • Kotlin 1.8
  • Kotlin Coroutines 1.7

问题描述

控制器定义了如下suspend函数:

@PutMapping("/{id}", consumes = [APPLICATION_JSON_VALUE])
suspend fun update(/* ... */): ResponseEntity<Lead> = service.update(/* ... */).okOrNotFound()

服务层执行实体查询、修改、保存操作时,若版本不匹配会抛出OptimisticLockingFailureException,但异常的栈跟踪信息丢失——原本应显示异常起源于repo.save的调用位置,实际却指向控制器函数。

无论使用返回Mono/Flux的R2dbcRepository,还是提供suspend函数的CoroutineCrudRepository,都会出现该问题。仅当异常跨Reactive/协程边界抛出时才会丢失栈跟踪,服务内部抛出的异常栈跟踪显示正常。

当前临时解决方案(代码较为冗余):

val lead = try {
    repo.save(updated)
} catch (@Suppress("SwallowedException") e: OptimisticLockingFailureException) {
    throw OptimisticLockingFailureException(e.message.orEmpty(), e.cause)
}

询问是否存在更优解决方案,是否需要让R2dbc实现的OptimisticLockingFailureException实现CopyThrowable接口。

解决方案

1. 让异常实现CopyThrowable接口

Kotlin协程在跨上下文传递异常时,依赖CopyThrowable接口来保留原始栈跟踪信息。如果OptimisticLockingFailureException实现该接口,协程会自动复制原始异常的栈信息,无需手动捕获重抛。

实现方式可选:

  • 自定义异常子类:创建继承OptimisticLockingFailureException并实现CopyThrowable的子类,在仓库保存操作抛出异常时替换为该自定义异常。
  • 异常转换扩展:若无法修改原始异常类,可通过Kotlin扩展函数,在捕获原始异常后转换为实现CopyThrowable的版本再抛出。

2. 全局异常处理器增强

在Spring WebFlux中配置全局异常处理器,捕获OptimisticLockingFailureException时手动合并原始栈跟踪:

@RestControllerAdvice
class GlobalExceptionHandler {
    @ExceptionHandler(OptimisticLockingFailureException::class)
    suspend fun handleOptimisticLockingFailure(ex: OptimisticLockingFailureException): ResponseEntity<ErrorResponse> {
        val enhancedException = OptimisticLockingFailureException(ex.message, ex.cause).apply {
            // 合并原始栈跟踪与当前栈信息
            stackTrace = ex.stackTrace + stackTrace
        }
        // 记录增强后的异常日志
        LoggerFactory.getLogger(javaClass).error("乐观锁冲突", enhancedException)
        return ResponseEntity.status(HttpStatus.CONFLICT)
            .body(ErrorResponse(message = "数据版本冲突,请刷新后重试"))
    }
}

data class ErrorResponse(val message: String)

3. 协程上下文配置增强

通过自定义协程Dispatcher并配置CoroutineExceptionHandler,统一处理异常并保留栈跟踪:

@Configuration
class CoroutineConfig {
    @Bean
    fun coroutineDispatcher(): CoroutineDispatcher {
        return Dispatchers.IO + CoroutineExceptionHandler { _, throwable ->
            val enhancedThrowable = if (throwable is OptimisticLockingFailureException) {
                OptimisticLockingFailureException(throwable.message, throwable.cause).apply {
                    stackTrace = throwable.stackTrace + stackTrace
                }
            } else throwable
            LoggerFactory.getLogger(javaClass).error("协程执行异常", enhancedThrowable)
        }
    }
}

结论

最优雅的解决方案是让OptimisticLockingFailureException实现CopyThrowable接口,这样协程框架会自动处理栈跟踪的保留,无需额外代码。若无法修改原始异常类,全局异常处理器或协程上下文增强的方式,比在每个仓库调用处手动捕获重抛更简洁易维护。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 22:10:38