首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Retrofit 网络请求的异常处理与重试策略实战

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

原创
作者头像
hunter android
发布于 2026-10-19 11:09:52
发布于 2026-10-19 11:09:52
430
举报

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 层、业务层的异常统一封装:

代码语言:javascript
复制
sealedclassApiException(message:String,cause:Throwable? =null):Exception(message,cause){
// 网络层异常
classNetworkException(cause:Throwable):ApiException("网络连接失败",cause)

// HTTP 异常
classHttpException(valcode:Int,message:String):ApiException(message)

// 业务异常
classBusinessException(valcode:Int,message:String):ApiException(message)

// 未知异常
classUnknownException(cause:Throwable):ApiException("未知错误",cause)
}

2.2 Response 统一解析

定义通用的响应体结构:

代码语言:javascript
复制
dataclassApiResponse<T>(
valcode:Int,
valmessage:String,
valdata:T?
)

创建 CallAdapter.Factory 将原始的 Call<ApiResponse<T>> 转换为 Call<T>,并在此处统一处理异常:

代码语言:javascript
复制
classApiCallAdapterFactory:CallAdapter.Factory(){
overridefunget(
returnType:Type,
annotations:Array<outAnnotation>,
retrofit:Retrofit
):CallAdapter<*,*>?{
if(getRawType(returnType)!=Call::class.java)returnnull

valresponseType=getParameterUpperBound(0,returnTypeasParameterizedType)
returnApiCallAdapter<Any>(responseType)
}

privateclassApiCallAdapter<R>(privatevalresponseType:Type):CallAdapter<ApiResponse<R>,Call<R>>{
overridefunresponseType():Type=responseType

overridefunadapt(call:Call<ApiResponse<R>>):Call<R>{
returnApiCall(call)
}
}
}

2.3 统一异常转换

在 ApiCall 的 enqueue 方法中,将所有异常转换为 ApiException:

代码语言:javascript
复制
classApiCall<T>(privatevaldelegate:Call<ApiResponse<T>>):Call<T>{

overridefunenqueue(callback:Callback<T>){
delegate.enqueue(object:Callback<ApiResponse<T>>{
overridefunonResponse(call:Call<ApiResponse<T>>,response:Response<ApiResponse<T>>){
if(response.isSuccessful){
valbody=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()))
}
}

overridefunonFailure(call:Call<ApiResponse<T>>,t:Throwable){
valexception=when(t){
isConnectException,isUnknownHostException->ApiException.NetworkException(t)
isSocketTimeoutException->ApiException.HttpException(408,"请求超时")
else->ApiException.UnknownException(t)
}
callback.onFailure(this@ApiCall,exception)
}
})
}

overridefunclone():Call<T>=ApiCall(delegate.clone())
overridefunexecute():Response<T>=throwUnsupportedOperationException("同步请求不支持")
overridefunisExecuted():Boolean=delegate.isExecuted
overridefuncancel()=delegate.cancel()
overridefunisCanceled():Boolean=delegate.isCanceled
overridefunrequest():Request=delegate.request()
overridefuntimeout():Timeout=delegate.timeout()
}

三、重试策略的实现

3.1 OkHttp Interceptor 实现自动重试

对于网络波动导致的临时性失败(如 5xx 错误、超时),可以在 Interceptor 中实现自动重试:

代码语言:javascript
复制
classRetryInterceptor(
privatevalmaxRetry:Int=3,
privatevalretryDelay:Long=1000L
):Interceptor{

overridefunintercept(chain:Interceptor.Chain):Response{
valrequest=chain.request()
varresponse:Response? =null
varexception:IOException? =null

repeat(maxRetry){attempt->
try{
response=chain.proceed(request)
// 如果是 5xx 错误,尝试重试
if(response!!.isSuccessful||response!!.code<500){
returnresponse!!
}
response!!.close()
}catch(e:IOException){
exception=e
}

// 最后一次不需要延迟
if(attempt<maxRetry-1){
Thread.sleep(retryDelay*(attempt+1))// 指数退避
}
}

// 所有重试都失败
throwexception?:IOException("Max retry reached")
}
}

配置到 OkHttpClient:

代码语言:javascript
复制
valclient=OkHttpClient.Builder()
.addInterceptor(RetryInterceptor(maxRetry=3,retryDelay=1000))
.build()

3.2 Token 过期自动刷新

对于 401 未授权的场景,通常需要刷新 Token 后重试:

代码语言:javascript
复制
classTokenInterceptor(
privatevaltokenManager:TokenManager
):Interceptor{

overridefunintercept(chain:Interceptor.Chain):Response{
valoriginalRequest=chain.request()

// 添加 Token
valrequestWithToken=originalRequest.newBuilder()
.header("Authorization","Bearer ${tokenManager.getAccessToken()}")
.build()

valresponse=chain.proceed(requestWithToken)

// 如果返回 401,尝试刷新 Token
if(response.code==401){
response.close()

synchronized(this){
valnewToken=tokenManager.refreshToken()

// 用新 Token 重试
valnewRequest=originalRequest.newBuilder()
.header("Authorization","Bearer $newToken")
.build()

returnchain.proceed(newRequest)
}
}

returnresponse
}
}

3.3 业务层重试(协程 + Flow)

对于需要在业务层控制的重试逻辑,可以使用 Kotlin Flow 的 retry 操作符:

代码语言:javascript
复制
suspendfunfetchUserInfo():Result<UserInfo>=flow{
valresponse=apiService.getUserInfo()
emit(response)
}.retry(retries=2){cause->
// 只对网络异常重试
causeisApiException.NetworkException
}.catch{e->
emit(Result.failure(e))
}.map{
Result.success(it)
}.first()

四、降级策略与本地缓存

当网络完全不可用时,可以返回本地缓存的数据:

代码语言:javascript
复制
classCacheInterceptor(privatevalcache:Cache):Interceptor{
overridefunintercept(chain:Interceptor.Chain):Response{
valrequest=chain.request()

returntry{
chain.proceed(request)
}catch(e:IOException){
// 网络失败,尝试读取缓存
valcacheResponse=cache.get(request)
if(cacheResponse!=null){
cacheResponse.newBuilder()
.header("Cache-Control","only-if-cached, max-stale=2592000")// 30天
.build()
}else{
throwe
}
}
}
}

五、完整的配置示例

代码语言:javascript
复制
valclient=OkHttpClient.Builder()
.addInterceptor(TokenInterceptor(tokenManager))
.addInterceptor(RetryInterceptor(maxRetry=3))
.addInterceptor(CacheInterceptor(cache))
.connectTimeout(10,TimeUnit.SECONDS)
.readTimeout(30,TimeUnit.SECONDS)
.build()

valretrofit=Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addCallAdapterFactory(ApiCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build()

六、实战中的坑

6.1 重试导致的重复请求

对于非幂等的 POST 请求(如下单、支付),不能无脑重试,需要根据业务场景判断:

代码语言:javascript
复制
valrequest=chain.request()
if(request.method!="GET"&&request.method!="HEAD"){
// POST/PUT/DELETE 等非幂等请求,不自动重试
returnchain.proceed(request)
}

6.2 Token 刷新的并发问题

多个请求同时收到 401 时,会触发多次 Token 刷新,需要加锁保证只刷新一次:

代码语言:javascript
复制
privatevallock=Mutex()

suspendfunrefreshToken():String{
lock.withLock{
// 检查是否已经被其他线程刷新过
if(isTokenValid())returngetAccessToken()

// 调用刷新接口
valnewToken=apiService.refreshToken(getRefreshToken())
saveAccessToken(newToken)
returnnewToken
}
}

6.3 指数退避避免服务雪崩

固定间隔的重试可能会在服务恢复瞬间造成流量洪峰,建议使用指数退避 + 随机抖动:

代码语言:javascript
复制
valdelay=retryDelay*(1shlattempt)+Random.nextLong(0,500)
Thread.sleep(delay)

七、总结

Retrofit 的异常处理与重试策略,需要在网络层、HTTP 层、业务层分层设计:

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

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • Retrofit 网络请求的异常处理与重试策略实战
    • 一、常见的网络异常场景
      • 1.1 网络层异常
      • 1.2 HTTP 层异常
      • 1.3 业务层异常
    • 二、Retrofit 异常处理的基础架构
      • 2.1 统一的异常封装
      • 2.2 Response 统一解析
      • 2.3 统一异常转换
    • 三、重试策略的实现
      • 3.1 OkHttp Interceptor 实现自动重试
      • 3.2 Token 过期自动刷新
      • 3.3 业务层重试(协程 + Flow)
    • 四、降级策略与本地缓存
    • 五、完整的配置示例
    • 六、实战中的坑
      • 6.1 重试导致的重复请求
      • 6.2 Token 刷新的并发问题
      • 6.3 指数退避避免服务雪崩
    • 七、总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档