在 Android 开发中,网络请求是绕不开的环节。Retrofit 作为主流的网络库,提供了简洁的 API 封装,但在实际项目中,我们经常会遇到网络超时、服务端 5xx 错误、Token 过期等异常场景。如何优雅地处理这些异常,并实现自动重试、降级策略,是工程实践中的重要一环。
本文将从真实的线上问题出发,梳理 Retrofit 异常处理与重试策略的完整方案。
首先定义一个统一的异常类,将网络层、HTTP 层、业务层的异常统一封装:
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)
}定义通用的响应体结构:
dataclassApiResponse<T>(
valcode:Int,
valmessage:String,
valdata:T?
)创建 CallAdapter.Factory 将原始的 Call<ApiResponse<T>> 转换为 Call<T>,并在此处统一处理异常:
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)
}
}
}在 ApiCall 的 enqueue 方法中,将所有异常转换为 ApiException:
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()
}对于网络波动导致的临时性失败(如 5xx 错误、超时),可以在 Interceptor 中实现自动重试:
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:
valclient=OkHttpClient.Builder()
.addInterceptor(RetryInterceptor(maxRetry=3,retryDelay=1000))
.build()对于 401 未授权的场景,通常需要刷新 Token 后重试:
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
}
}对于需要在业务层控制的重试逻辑,可以使用 Kotlin Flow 的 retry 操作符:
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()当网络完全不可用时,可以返回本地缓存的数据:
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
}
}
}
}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()对于非幂等的 POST 请求(如下单、支付),不能无脑重试,需要根据业务场景判断:
valrequest=chain.request()
if(request.method!="GET"&&request.method!="HEAD"){
// POST/PUT/DELETE 等非幂等请求,不自动重试
returnchain.proceed(request)
}多个请求同时收到 401 时,会触发多次 Token 刷新,需要加锁保证只刷新一次:
privatevallock=Mutex()
suspendfunrefreshToken():String{
lock.withLock{
// 检查是否已经被其他线程刷新过
if(isTokenValid())returngetAccessToken()
// 调用刷新接口
valnewToken=apiService.refreshToken(getRefreshToken())
saveAccessToken(newToken)
returnnewToken
}
}固定间隔的重试可能会在服务恢复瞬间造成流量洪峰,建议使用指数退避 + 随机抖动:
valdelay=retryDelay*(1shlattempt)+Random.nextLong(0,500)
Thread.sleep(delay)Retrofit 的异常处理与重试策略,需要在网络层、HTTP 层、业务层分层设计:
ApiExceptionretry 实现灵活的重试逻辑在实际项目中,还需要根据业务场景判断哪些请求可以重试、重试几次、如何避免雪崩,这些都需要结合监控数据和线上反馈不断优化。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。