Spring Boot WebClient协程forEach循环请求JobCancellationException处理方案
问题分析
你遇到的核心问题是:在Kotlin协程中使用WebClient的awaitExchange时,单个请求的异常(尤其是JobCancellationException)会触发协程取消信号,进而导致整个循环的后续请求被阻塞。即使你用了try/catch,由于协程上下文的取消传播机制,以及WebClient响应式流的取消信号会影响连接池,最终导致后续请求无法正常执行。
解决方案
1. 用supervisorScope隔离协程上下文
默认的协程上下文(如coroutineScope)中,单个子协程失败会取消所有同级子协程及父协程。改用supervisorScope可以让单个请求的异常仅影响自身协程,不会传播到其他请求:
runBlocking { supervisorScope { // 假设你的请求体列表为requestBodies requestBodies.forEach { body -> launch(Dispatchers.IO) { // 用IO调度器处理网络IO操作 try { webClient.post() .uri("你的目标请求URL") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .bodyValue(body) .awaitExchange { response -> // 响应处理阶段也要捕获异常,避免触发Mono取消信号 try { when { response.statusCode().is2xxSuccessful -> { val result = response.awaitBody<YourResponseType>() // 处理成功响应逻辑 println("请求成功:$result") } response.statusCode().is4xxClientError -> { println("客户端错误:${response.statusCode()}") } else -> { println("服务端错误:${response.statusCode()}") } } } catch (e: Exception) { println("响应处理失败:${e.message}") } } } catch (e: JobCancellationException) { // 显式捕获协程取消异常,避免向上传播 println("请求被取消:${e.message}") } catch (e: Exception) { // 捕获其他HTTP相关异常 println("请求异常:${e.message}") } } } } }
2. 配置WebClient的超时与连接池
单个请求的超时或连接耗尽也会触发JobCancellationException,需要给WebClient配置合理的超时和连接池参数,避免影响全局连接资源:
val httpClient = HttpClient.create() // 设置连接超时时间 .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // 设置响应超时时间 .responseTimeout(Duration.ofSeconds(10)) // 配置连接池参数 .poolConfig(PoolConfig.builder() .maxConnections(15) // 根据并发量调整最大连接数 .pendingAcquireTimeout(Duration.ofSeconds(5)) // 等待获取连接的超时时间 .build()) val webClient = WebClient.builder() .clientConnector(ReactorClientHttpConnector(httpClient)) .build()
3. 替换awaitExchange为awaitBody(可选)
如果不需要手动处理非2xx状态码,可以用awaitBody简化代码,它会自动处理2xx响应,非2xx状态会抛出WebClientResponseException,更容易统一捕获:
try { val result = webClient.post() .uri("你的目标请求URL") .contentType(MediaType.APPLICATION_JSON) .bodyValue(body) .awaitBody<YourResponseType>() println("请求成功:$result") } catch (e: WebClientResponseException) { println("请求失败,状态码:${e.statusCode()},信息:${e.message}") } catch (e: JobCancellationException) { println("请求被取消:${e.message}") } catch (e: Exception) { println("请求异常:${e.message}") }
关键注意点
- 必须在响应处理的lambda内部也添加try/catch,否则处理响应时的异常会触发Mono的取消信号,进而导致连接关闭。
supervisorScope是核心,它保证了单个请求的协程失败不会取消其他请求的协程。- 显式捕获
JobCancellationException,防止该异常未被处理而传播到父协程。
内容的提问来源于stack exchange,提问作者Hantsy
相关产品推荐
相关产品推荐

