跨Reactive/协程边界时异常栈跟踪丢失问题求助
环境
- 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

