Retrofit 网络请求的异常处理与重试策略实战
在 Android 开发中,网络请求是绕不开的环节。Retrofit 作为主流的网络库,提供了简洁的 API 封装,但在实际项目中,我们经常会遇到网络超时、服务端 5xx 错误、Token 过期等异常场景。如何优雅地处理这些异常,并实现自动重试、降级策略,是工程实践中的重要一环。
本文将从真实的线上问题出发,梳理 Retrofit 异常处理与重试策略的完整方案。
一、常见的网络异常场景
1.1 网络层异常
- ConnectException:网络连接失败(如 Wi-Fi 断开、飞行模式)
- SocketTimeoutException:连接或读取超时
- UnknownHostException:DNS 解析失败
1.2 HTTP 层异常
- 4xx 客户端错误:401 未授权、403 禁止访问、404 资源不存在
- 5xx 服务端错误:500 内部错误、502 网关错误、503 服务不可用
1.3 业务层异常
- Token 过期:需要刷新 Token 后重试
- 签名校验失败:参数被篡改或时间戳过期
- 业务错误码:如余额不足、库存不足等
二、Retrofit 异常处理的基础架构
2.1 统一的异常封装
首先定义一个统一的异常类,将网络层、HTTP 层、业务层的异常统一封装:
kotlin
sealed class ApiException(message: String, cause: Throwable? = null) : Exception(message, cause) {
// 网络层异常
class NetworkException(cause: Throwable) : ApiException("网络连接失败", cause)
// HTTP 异常
class HttpException(val code: Int, message: String) : ApiException(message)
// 业务异常
class BusinessException(val code: Int, message: String) : ApiException(message)
// 未知异常
class UnknownException(cause: Throwable) : ApiException("未知错误", cause)
}
2.2 Response 统一解析
定义通用的响应体结构:
kotlin
data class ApiResponse<T>(
val code: Int,
val message: String,
val data: T?
)
创建 CallAdapter.Factory 将原始的 Call<ApiResponse<T>> 转换为 Call<T>,并在此处统一处理异常:
kotlin
class ApiCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (getRawType(returnType) != Call::class.java) return null
val responseType = getParameterUpperBound(0, returnType as ParameterizedType)
return ApiCallAdapter<Any>(responseType)
}
private class ApiCallAdapter<R>(private val responseType: Type) : CallAdapter<ApiResponse<R>, Call<R>> {
override fun responseType(): Type = responseType
override fun adapt(call: Call<ApiResponse<R>>): Call<R> {
return ApiCall(call)
}
}
}
2.3 统一异常转换
在 ApiCall 的 enqueue 方法中,将所有异常转换为 ApiException:
kotlin
class ApiCall<T>(private val delegate: Call<ApiResponse<T>>) : Call<T> {
override fun enqueue(callback: Callback<T>) {
delegate.enqueue(object : Callback<ApiResponse<T>> {
override fun onResponse(call: Call<ApiResponse<T>>, response: Response<ApiResponse<T>>) {
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
when (body.code) {
200 -> callback.onResponse(this@ApiCall, Response.success(body.data))
401 -> callback.onFailure(this@ApiCall, ApiException.HttpException(401, "未授权"))
else -> callback.onFailure(this@ApiCall, ApiException.BusinessException(body.code, body.message))
}
} else {
callback.onFailure(this@ApiCall, ApiException.UnknownException(NullPointerException("Response body is null")))
}
} else {
callback.onFailure(this@ApiCall, ApiException.HttpException(response.code(), response.message()))
}
}
override fun onFailure(call: Call<ApiResponse<T>>, t: Throwable) {
val exception = when (t) {
is ConnectException, is UnknownHostException -> ApiException.NetworkException(t)
is SocketTimeoutException -> ApiException.HttpException(408, "请求超时")
else -> ApiException.UnknownException(t)
}
callback.onFailure(this@ApiCall, exception)
}
})
}
override fun clone(): Call<T> = ApiCall(delegate.clone())
override fun execute(): Response<T> = throw UnsupportedOperationException("同步请求不支持")
override fun isExecuted(): Boolean = delegate.isExecuted
override fun cancel() = delegate.cancel()
override fun isCanceled(): Boolean = delegate.isCanceled
override fun request(): Request = delegate.request()
override fun timeout(): Timeout = delegate.timeout()
}
三、重试策略的实现
3.1 OkHttp Interceptor 实现自动重试
对于网络波动导致的临时性失败(如 5xx 错误、超时),可以在 Interceptor 中实现自动重试:
kotlin
class RetryInterceptor(
private val maxRetry: Int = 3,
private val retryDelay: Long = 1000L
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var response: Response? = null
var exception: IOException? = null
repeat(maxRetry) { attempt ->
try {
response = chain.proceed(request)
// 如果是 5xx 错误,尝试重试
if (response!!.isSuccessful || response!!.code < 500) {
return response!!
}
response!!.close()
} catch (e: IOException) {
exception = e
}
// 最后一次不需要延迟
if (attempt < maxRetry - 1) {
Thread.sleep(retryDelay * (attempt + 1)) // 指数退避
}
}
// 所有重试都失败
throw exception ?: IOException("Max retry reached")
}
}
配置到 OkHttpClient:
kotlin
val client = OkHttpClient.Builder()
.addInterceptor(RetryInterceptor(maxRetry = 3, retryDelay = 1000))
.build()
3.2 Token 过期自动刷新
对于 401 未授权的场景,通常需要刷新 Token 后重试:
kotlin
class TokenInterceptor(
private val tokenManager: TokenManager
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
// 添加 Token
val requestWithToken = originalRequest.newBuilder()
.header("Authorization", "Bearer ${tokenManager.getAccessToken()}")
.build()
val response = chain.proceed(requestWithToken)
// 如果返回 401,尝试刷新 Token
if (response.code == 401) {
response.close()
synchronized(this) {
val newToken = tokenManager.refreshToken()
// 用新 Token 重试
val newRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
return chain.proceed(newRequest)
}
}
return response
}
}
3.3 业务层重试(协程 + Flow)
对于需要在业务层控制的重试逻辑,可以使用 Kotlin Flow 的 retry 操作符:
kotlin
suspend fun fetchUserInfo(): Result<UserInfo> = flow {
val response = apiService.getUserInfo()
emit(response)
}.retry(retries = 2) { cause ->
// 只对网络异常重试
cause is ApiException.NetworkException
}.catch { e ->
emit(Result.failure(e))
}.map {
Result.success(it)
}.first()
四、降级策略与本地缓存
当网络完全不可用时,可以返回本地缓存的数据:
kotlin
class CacheInterceptor(private val cache: Cache) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
return try {
chain.proceed(request)
} catch (e: IOException) {
// 网络失败,尝试读取缓存
val cacheResponse = cache.get(request)
if (cacheResponse != null) {
cacheResponse.newBuilder()
.header("Cache-Control", "only-if-cached, max-stale=2592000") // 30天
.build()
} else {
throw e
}
}
}
}
五、完整的配置示例
kotlin
val client = OkHttpClient.Builder()
.addInterceptor(TokenInterceptor(tokenManager))
.addInterceptor(RetryInterceptor(maxRetry = 3))
.addInterceptor(CacheInterceptor(cache))
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addCallAdapterFactory(ApiCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build()
六、实战中的坑
6.1 重试导致的重复请求
对于非幂等的 POST 请求(如下单、支付),不能无脑重试,需要根据业务场景判断:
kotlin
val request = chain.request()
if (request.method != "GET" && request.method != "HEAD") {
// POST/PUT/DELETE 等非幂等请求,不自动重试
return chain.proceed(request)
}
6.2 Token 刷新的并发问题
多个请求同时收到 401 时,会触发多次 Token 刷新,需要加锁保证只刷新一次:
kotlin
private val lock = Mutex()
suspend fun refreshToken(): String {
lock.withLock {
// 检查是否已经被其他线程刷新过
if (isTokenValid()) return getAccessToken()
// 调用刷新接口
val newToken = apiService.refreshToken(getRefreshToken())
saveAccessToken(newToken)
return newToken
}
}
6.3 指数退避避免服务雪崩
固定间隔的重试可能会在服务恢复瞬间造成流量洪峰,建议使用指数退避 + 随机抖动:
kotlin
val delay = retryDelay * (1 shl attempt) + Random.nextLong(0, 500)
Thread.sleep(delay)
七、总结
Retrofit 的异常处理与重试策略,需要在网络层、HTTP 层、业务层分层设计:
- 统一异常封装 :将不同层级的异常转换为统一的
ApiException - Interceptor 自动重试:对临时性故障(5xx、超时)自动重试
- Token 自动刷新:对 401 异常透明刷新 Token 并重试
- 降级策略:网络不可用时返回本地缓存
- 业务层控制 :用 Flow 的
retry实现灵活的重试逻辑
在实际项目中,还需要根据业务场景判断哪些请求可以重试、重试几次、如何避免雪崩,这些都需要结合监控数据和线上反馈不断优化。