Retrofit 网络请求的异常处理与重试策略实战

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 层、业务层分层设计:

  1. 统一异常封装 :将不同层级的异常转换为统一的 ApiException
  2. Interceptor 自动重试:对临时性故障(5xx、超时)自动重试
  3. Token 自动刷新:对 401 异常透明刷新 Token 并重试
  4. 降级策略:网络不可用时返回本地缓存
  5. 业务层控制 :用 Flow 的 retry 实现灵活的重试逻辑

在实际项目中,还需要根据业务场景判断哪些请求可以重试、重试几次、如何避免雪崩,这些都需要结合监控数据和线上反馈不断优化。

相关推荐
sejinan1 小时前
极致轻量与秒级起播:NativeWasmTv 开源硬核技术解析与升级版体验
android·开源·webview·tv
小羊没烦恼!2 小时前
Ajax与JSON的一些总结
android·java·前端·数据库·mysql·算法·okhttp
mmsx2 小时前
Android 静默失败实录:五个没有日志的线上事故
android·kotlin
hai_android3 小时前
从零实现一个支持跨进程(IPC)的自定义 Toast
android·kotlin·android jetpack
龙腾-虎跃3 小时前
基于 SIP 协议的自研电话终端:FreeSWITCH 语音与视频通信实战
android·音视频·freeswitch·sip协议终端·wertc
imDwAaY3 小时前
操作系统如何管理程序运行?从线程、进程到内存隔离与用户态切换:CS162 Note2
android·java·redis
andyweike3 小时前
Android文件系统
android
studyForMokey4 小时前
【Android 面试】Paging3
android·面试·职场和发展
企业数字化笔记4 小时前
租户停用后数据和任务怎么处理?冻结、恢复与保留策略的实现
android·java·spring boot