3. 单次上报流程实现:使用协程进行HTTP POST、超时控制、异常捕获
3.1 章节目标与设计原则
单次上报是整个埋点重试机制的基础单元。本章将实现一个使用Kotlin协程的HTTP POST上报函数,包含:
- 超时控制:防止网络抖动导致长时间阻塞协程调度池。
- 异常捕获:区分网络异常、服务器错误、数据序列化错误,为后续重试策略提供精确错误类型。
- 协程安全:确保在Dispatchers.IO上执行网络请求,不阻塞主线程。
核心设计哲学: 单次上报函数应是“幂等且无副作用”的——它的唯一职责是发送一次请求并返回结果状态,绝不自行决定重试或缓存。
3.2 实现流程总览
下图展示了单次上报的完整流程,从协程启动到结果分派。
3.3 关键代码实现
下面给出完整的单次上报函数,使用OkHttp作为HTTP客户端,Kotlin协程进行异步控制。
suspend fun reportSingleEvent(
event: Event,
client: OkHttpClient,
url: String,
timeoutMillis: Long = 5000
): Result<Unit> {
return withContext(Dispatchers.IO) {
try {
// 超时控制
withTimeout(timeoutMillis) {
val jsonBody = event.toJson().toRequestBody(MediaType.parse("application/json")!!)
val request = Request.Builder()
.url(url)
.post(jsonBody)
.build()
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
throw HttpException(response.code, response.message)
}
response.close()
Result.success(Unit)
}
} catch (e: TimeoutCancellationException) {
// 超时异常单独处理
Result.failure(EventTimeoutException("上报超时: $timeoutMillis ms", e))
} catch (e: HttpException) {
Result.failure(EventHttpException("HTTP错误: ${e.code}", e))
} catch (e: IOException) {
Result.failure(EventNetworkException("网络异常: ${e.message}", e))
} catch (e: Exception) {
Result.failure(EventUnknownException("未知错误: ${e.message}", e))
}
}
}
// 自定义异常类示例
class EventTimeoutException(msg: String, cause: Throwable?) : Exception(msg, cause)
class EventHttpException(msg: String, cause: Throwable?) : Exception(msg, cause)
class EventNetworkException(msg: String, cause: Throwable?) : Exception(msg, cause)
class EventUnknownException(msg: String, cause: Throwable?) : Exception(msg, cause)
代码要点:
withContext(Dispatchers.IO)确保网络请求不阻塞主线程。withTimeout()包裹整个请求,超时后抛出TimeoutCancellationException。- 自定义异常类(如
EventTimeoutException)便于上层重试策略区分错误类型。 - 返回值使用
Result<Unit>,符合Kotlin函数式风格,不强迫调用者try-catch。
3.4 异常类型与处理策略对照表
| 异常类型 | 触发场景 | 推荐重试策略 |
|---|---|---|
EventTimeoutException |
请求在指定时间内未收到响应 | 可重试,建议指数退避 |
EventHttpException |
服务器返回非2xx状态码 | 4xx一般不重试;5xx可重试 |
EventNetworkException |
DNS解析、连接拒绝、Socket异常 | 可重试,但需限制重试次数 |
EventUnknownException |
JSON序列化、协程取消等意外错误 | 不重试或根据具体上下文决定 |
重要注意事项:
- 不要将协程取消(CancellationException)捕获后吞掉,否则会破坏协程协作取消机制。上面的代码保留了
CancellationException的隐式传播(withTimeout抛出的是TimeoutCancellationException继承自CancellationException)。 - OkHttp的
Response必须调用close()或使用use块,否则连接不会被回收。 - 建议对
event.toJson()单独进行try-catch,以隔离序列化异常。
3.5 超时控制深度分析
协程的超时控制通过 withTimeout 实现,原理是启动一个内部的协程计时器,在指定时间后抛出一个 TimeoutCancellationException。这个异常是 CancellationException 的子类,所以它不会导致协程的父协程被取消,除非父协程捕获并忽略。
在实际埋点上报场景中,超时时间建议设置在 3~10秒 之间,过长会导致协程池线程被长时间占用,过短则可能因网络慢导致正常请求失败。
3.6 异常捕获最佳实践
捕获异常时应遵循“精确先行、兜底在后”的原则:
- 先捕获具体子类异常(如
TimeoutCancellationException)。 - 再捕获中间层异常(如
HttpException、IOException)。 - 最后用
Exception捕获所有未被覆盖的异常。 - 捕获后均包装为自定义异常并返回
Result.failure,保留原始 cause 以便调试。
// 精确捕获示例(已被整合到上方函数中)
try {
// ... 请求逻辑
} catch (e: TimeoutCancellationException) {
// 超时
} catch (e: HttpException) {
// HTTP错误
} catch (e: IOException) {
// 网络错误
} catch (e: Exception) {
// 兜底
}
调试技巧: 在开发阶段,可以开启 OkHttp 的日志拦截器来查看实际网络请求和响应细节。但在生产环境中应关闭或使用 DEBUG 级别避免敏感信息泄露。
3.7 单元测试验证
测试单次上报函数的核心是验证超时和异常分支的正确性。使用 MockWebServer(OkHttp内置)可以模拟不同场景:
// 模拟超时场景
val mockServer = MockWebServer()
mockServer.enqueue(MockResponse().setBodyDelay(10, TimeUnit.SECONDS)) // 延迟10秒
mockServer.start()
val client = OkHttpClient.Builder()
.connectTimeout(1, TimeUnit.SECONDS)
.readTimeout(1, TimeUnit.SECONDS)
.build()
val result = reportSingleEvent(
event = testEvent,
client = client,
url = mockServer.url("/").toString(),
timeoutMillis = 2000
)
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is EventTimeoutException)
mockServer.shutdown()
通过上述实现,单次上报函数具备了超时控制、精确异常分类和协程安全三大特性,为后续的重试机制提供了可靠的基础单元。