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 形态:入参为Call与Continuation,返回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()) } }三个关键点需要理解:
executeAsync()是Call的扩展挂起函数:它把一次异步请求包装成可挂起的调用,协程挂起直到响应到达或请求失败;- 读取响应体时切换到
Dispatchers.IO:因为网络栈本身不占用协程线程,但读取 body 是阻塞式 IO 操作,推荐放到 IO 调度器上执行; 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"——请求调度、连接池、重试等行为与普通异步调用完全一致,而不是启动一个协程去执行;onFailure→continuation.resumeWithException(e):网络失败(IOException,如连接被重置、超时)时,将异常原样抛给协程调用方,符合挂起函数的错误处理直觉;onResponse→continuation.resume(response) { _, value, _ -> value.closeQuietly() }:注意这里使用了resume的onCancellation参数。如果响应已经返回、但协程在消费它之前被取消(即 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 被取消时通常会以IOException(Canceled异常)触发onFailure,从而经resumeWithException抛出。所以在实际项目中,对executeAsync()的异常处理建议同时覆盖IOException(网络/取消)与CancellationException(协程协作取消)两类情形。
资源安全:协程取消时响应体不会泄漏
协程场景最容易踩的坑是:响应已经返回、协程却在读取前被取消,导致ResponseBody从未关闭、连接无法归还连接池。本模块从实现与版本历史两个层面都做了保障:
- 实现层面:
continuation.resume(response) { _, value, _ -> value.closeQuietly() }的onCancellation回调会在协程已取消的情况下自动关闭响应体; - 测试层面:ExecuteAsyncTest.kt 的
responseClosedIfCoroutineCanceled测试专门构造了一个ClosableCall,在回调返回后立刻coroutineContext.job.cancel()取消协程,最后断言call.canceled为true且call.responseClosed为true,证明响应体确实被关闭; - 版本历史层面:CHANGELOG.md 中记录了该模块曾经的修复:"Fix in
okhttp-coroutines: Don't leak response bodies inexecuteAsync(). We had a bug where ..."——这正说明响应体泄漏曾经是真实缺陷,当前实现已针对性修复。
此外,即使协程没有被取消,也强烈建议像 README 示例那样使用use {}显式关闭响应,双保险地避免泄漏。
错误处理与超时:完整的实战模式
综合源码与测试,executeAsync()的错误传播路径如下:
| 场景 | 触发机制 | 协程侧观察到的异常 |
|---|---|---|
| 网络失败/连接被关闭 | 底层onFailure→resumeWithException(e) | IOException |
主动call.cancel() | OkHttp 取消回调 | IOException(底层Canceled),见cancelledCall测试 |
withTimeout/ 作用域取消 | invokeOnCancellation→call.cancel() | TimeoutCancellationException等CancellationException,见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),仅供参考