OkHttp Coroutines 模块指南:用 `Call.executeAsync()` 在 Kotlin 协程中发起 HTTP 请求并理解双向取消语义
2026/9/18 15:34:57 网站建设 项目流程

OkHttp Coroutines 模块指南:用Call.executeAsync()在 Kotlin 协程中发起 HTTP 请求并理解双向取消语义

【免费下载链接】okhttpA meticulous HTTP client for the JVM, Android, and GraalVM.项目地址: https://gitcode.com/gh_mirrors/okh/okhttp

OkHttp 的okhttp-coroutines是面向 Kotlin 协程客户端的可选扩展模块,它为核心 OkHttp 库的Call增加了一个suspend扩展函数executeAsync(),让你能以挂起函数的方式发起异步请求,同时保持 OkHttp 自带 Dispatcher 的调度模型。读完本文,你将掌握该模块的引入方式、底层实现原理(基于suspendCancellableCoroutine)、协程与 Call 之间的双向取消行为,以及如何借助仓库内测试用例规避响应体泄漏等资源安全问题。

模块定位:一个为 Kotlin 协程提供挂起式 API 的可选构件

在 OkHttp 仓库中,okhttp-coroutines是一个独立、可选的模块(见 settings.gradle.kts 中的include(":okhttp-coroutines")),其模块描述见 okhttp-coroutines/README.md:"Support for Kotlin clients using coroutines."(为使用协程的 Kotlin 客户端提供支持)。

该模块的 Java 模块描述文件 okhttp-coroutines/src/main/java9/module-info.java 显示它只依赖并导出极少的 API:

module okhttp3.coroutines { requires okhttp3; exports okhttp3.coroutines; }

也就是说,整个模块对外只暴露okhttp3.coroutines包下一个核心能力。从已发布的 API 二进制接口 okhttp-coroutines/api/okhttp-coroutines.api 可以看到全部公共 API 仅此一个:

public final class okhttp3/coroutines/ExecuteAsyncKt { public static final fun executeAsync (Lokhttp3/Call;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; }

这是一个标准的suspend fun的 JVM 形态:入参为CallContinuation,返回Response。因此可以概括为:okhttp-coroutines 就是把 OkHttp 的异步回调式Call.enqueue(Callback)包装成一个挂起函数,供协程场景直接调用。

依赖方面,仓库在 gradle/libs.versions.toml 中声明coroutines = "1.11.0",并在 CHANGELOG.md 的 5.4.0 版本说明中明确:"Upgrade: kotlinx.coroutines 1.11.0. This is used by the optionalokhttp-coroutinesartifact.",即该模块是可选的,仅当你需要协程扩展时才引入。

快速上手:README 中的最小用法

根据 okhttp-coroutines/README.md 提供的示例,最小用法如下:

val call = client.newCall(request) call.executeAsync().use { response -> withContext(Dispatchers.IO) { println(response.body?.string()) } }

三个关键点需要理解:

  1. executeAsync()Call的扩展挂起函数:它把一次异步请求包装成可挂起的调用,协程挂起直到响应到达或请求失败;
  2. 读取响应体时切换到Dispatchers.IO:因为网络栈本身不占用协程线程,但读取 body 是阻塞式 IO 操作,推荐放到 IO 调度器上执行;
  3. use {}确保响应被释放Response实现了Closeable,用use包裹可以保证无论成功还是异常都会关闭响应体,这是防止连接泄漏的基本功。

源码剖析:suspendCancellableCoroutine背后的完整实现

整个模块的核心实现只有几十行,位于 okhttp-coroutines/src/main/kotlin/okhttp3/coroutines/ExecuteAsync.kt:

suspend fun Call.executeAsync(): Response = suspendCancellableCoroutine { continuation -> continuation.invokeOnCancellation { this.cancel() } this.enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { continuation.resumeWithException(e) } override fun onResponse( call: Call, response: Response, ) { continuation.resume(response) { _, value, _ -> value.closeQuietly() } } }, ) }

逐行拆解其设计意图:

  • suspendCancellableCoroutine:与普通suspendCoroutine不同,它是"可取消的",这为下文双向取消语义提供了基础能力;
  • continuation.invokeOnCancellation { this.cancel() }:注册取消回调——一旦承载本次调用的协程被取消,就同步调用底层Call.cancel(),把协程取消传递给 OkHttp 网络栈;
  • this.enqueue(Callback):内部仍走 OkHttp 标准的enqueue异步路径。这正是 README 中强调的"使用 OkHttp 的标准 Dispatcher"——请求调度、连接池、重试等行为与普通异步调用完全一致,而不是启动一个协程去执行;
  • onFailurecontinuation.resumeWithException(e):网络失败(IOException,如连接被重置、超时)时,将异常原样抛给协程调用方,符合挂起函数的错误处理直觉;
  • onResponsecontinuation.resume(response) { _, value, _ -> value.closeQuietly() }:注意这里使用了resumeonCancellation参数。如果响应已经返回、但协程在消费它之前被取消(即 continuation 已取消、无法再恢复),那么onCancellation会被执行,自动调用closeQuietly()关闭响应体。

调度模型:为什么默认不使用 Kotlin 的 Dispatchers

这是 README 中特别强调的一个设计点:"This is implemented usingsuspendCancellableCoroutinebut uses the standard Dispatcher in OkHttp. This means that by default Kotlin's Dispatchers are not used."

含义如下:

  • executeAsync()只是把Call.enqueue()的异步回调"翻译"成挂起恢复,真正的网络调度仍由 OkHttp 的 Dispatcher(默认线程池/连接池体系)完成;
  • 因此你不需要(也不应该)用withContext(Dispatchers.IO)去包裹executeAsync()本身——发起请求不阻塞任何协程线程;
  • 唯一建议使用Dispatchers.IO的地方是读取响应体(如response.body?.string()),因为 body 的流式读取是阻塞操作。README 示例正是这样组织的。

这种设计的好处是:OkHttp 的连接复用、超时、重试、拦截器等既有机制全部原样生效,协程只是薄薄的一层语法糖。

双向取消:协程取消 Call、Call 取消协程

README 用三句话概括了取消语义:

Cancellation is implemented sensibly in both directions. Cancelling a coroutine scope will cancel the call. Cancelling a call will throw a CancellationException but not cancel the scope if caught.

结合源码与测试可以验证这三条:

方向一:取消协程作用域 → 取消 Call。源码中invokeOnCancellation { this.cancel() }保证这一点。ExecuteAsyncTest.kt 的timeoutCall测试用withTimeout(1.seconds)模拟超时(服务端通过MockResponse.Builder().bodyDelay(5, TimeUnit.SECONDS)故意延迟响应),断言抛出TimeoutCancellationException,并验证call.isCanceled()true——即作用域取消确实传递到了 Call。

方向二:取消 Call → 协程抛 CancellationException。但注意 README 的措辞:"Cancelling a call will throw aCancellationExceptionbut not cancel the scope if caught.",即主动call.cancel()时,挂起点会以取消异常唤醒,但如果调用方捕获了这个异常,外层作用域不会被牵连取消。这与协程标准的取消协作模型一致。

关于取消引发的实际异常类型,一个细节值得说明:在cancelledCall测试(ExecuteAsyncTest.kt)中,测试调用call.cancel()后断言抛出的是IOException并验证call.isCanceled()true。这源于 OkHttp 底层enqueue回调在 Call 被取消时通常会以IOExceptionCanceled异常)触发onFailure,从而经resumeWithException抛出。所以在实际项目中,对executeAsync()的异常处理建议同时覆盖IOException(网络/取消)与CancellationException(协程协作取消)两类情形。

资源安全:协程取消时响应体不会泄漏

协程场景最容易踩的坑是:响应已经返回、协程却在读取前被取消,导致ResponseBody从未关闭、连接无法归还连接池。本模块从实现与版本历史两个层面都做了保障:

  • 实现层面continuation.resume(response) { _, value, _ -> value.closeQuietly() }onCancellation回调会在协程已取消的情况下自动关闭响应体;
  • 测试层面:ExecuteAsyncTest.kt 的responseClosedIfCoroutineCanceled测试专门构造了一个ClosableCall,在回调返回后立刻coroutineContext.job.cancel()取消协程,最后断言call.canceledtruecall.responseClosedtrue,证明响应体确实被关闭;
  • 版本历史层面:CHANGELOG.md 中记录了该模块曾经的修复:"Fix inokhttp-coroutines: Don't leak response bodies inexecuteAsync(). We had a bug where ..."——这正说明响应体泄漏曾经是真实缺陷,当前实现已针对性修复。

此外,即使协程没有被取消,也强烈建议像 README 示例那样使用use {}显式关闭响应,双保险地避免泄漏。

错误处理与超时:完整的实战模式

综合源码与测试,executeAsync()的错误传播路径如下:

场景触发机制协程侧观察到的异常
网络失败/连接被关闭底层onFailureresumeWithException(e)IOException
主动call.cancel()OkHttp 取消回调IOException(底层Canceled),见cancelledCall测试
withTimeout/ 作用域取消invokeOnCancellationcall.cancel()TimeoutCancellationExceptionCancellationException,见timeoutCall测试

failedCall测试(ExecuteAsyncTest.kt)使用SocketEffect.ShutdownConnection模拟服务端在响应开始后立刻断开连接,验证连接中途被重置时同样抛出IOException

基于以上行为,一个健壮的调用范式可以组织为:

suspend fun fetch(client: OkHttpClient, url: String): String { val request = Request.Builder().url(url).build() val call = client.newCall(request) // 1) executeAsync 内部走 OkHttp 标准 Dispatcher,无需包 IO 上下文 // 2) 需要整体超时保护时用 withTimeout(会级联取消 Call) return withTimeout(10.seconds) { call.executeAsync().use { response -> // 3) 读取 body 属于阻塞 IO,放到 Dispatchers.IO withContext(Dispatchers.IO) { response.body!!.string() } } } }

版本演进:该模块在 OkHttp 中的历史轨迹

CHANGELOG.md 记录了okhttp-coroutines模块的关键演进,可帮助你理解其成熟度:

  • 5.4.0:随 OkHttp 升级 kotlinx.coroutines 至 1.11.0(该依赖仅服务于本可选构件);
  • Breaking 变更:协程扩展被移入okhttp3.coroutines包(此前与 OkHttp 核心包混用),使用旧包名的代码需要迁移;
  • 修复:修复executeAsync()泄漏响应体的问题(前文已述);修复 5.0.0-alpha.13 中 JAR 构件发布不合法的问题;
  • API 收敛:停止使用协程库的实验性 API,对外接口更稳定。

小结

OkHttp 的okhttp-coroutines模块用最精简的 API(一个suspend扩展函数)为 Kotlin 协程客户端补齐了挂起式请求能力:它基于suspendCancellableCoroutine实现,复用 OkHttp 标准 Dispatcher 与完整网络栈,提供双向取消语义,并通过onCancellation关闭机制规避响应体泄漏。阅读本文后,你可以直接查阅 ExecuteAsync.kt 的完整实现,并用 ExecuteAsyncTest.kt 中的五个测试用例验证超时、取消、失败与资源关闭等全部行为。

【免费下载链接】okhttpA meticulous HTTP client for the JVM, Android, and GraalVM.项目地址: https://gitcode.com/gh_mirrors/okh/okhttp

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询