ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Retrofit + OkHttp 二次封装实战:打造统一异常处理与 Token 自动刷新的网络框架

2026/8/10 10:12:03 拓冰建站 浏览量
Retrofit + OkHttp 二次封装实战:打造统一异常处理与 Token 自动刷新的网络框架 Retrofit OkHttp 二次封装实战打造统一异常处理与 Token 自动刷新的网络框架前言为什么要二次封装原生 Retrofit OkHttp 已经非常强大但在实际企业级项目中直接使用会带来不少问题每个业务都手写CallAdapter、Interceptor重复代码极多异常分散在onFailure、onResponse各处难以统一处理Token 过期逻辑散落在各个页面刷新逻辑混乱日志、超时、重试策略无法统一管理业务错误码如 200 但code ! 0处理不一致因此二次封装一个通用网络请求框架是大型 App 架构中必不可少的一环。本文将围绕以下目标展开✅ 统一请求 / 响应模型✅ 统一异常处理HTTP / 业务 / 网络✅ Token 自动刷新无感续期✅ 支持多 TokenAccess Token / Refresh Token✅ 线程安全 防并发刷新✅ 易用、可扩展、可测试一、整体设计思路1. 架构分层App └── Network Layer ├── RetrofitManager // Retrofit 统一入口 ├── OkHttpProvider // OkHttp 配置中心 ├── ApiService // 业务接口定义 ├── ResponseDTO // 统一响应模型 ├── ExceptionHandler // 异常转换与处理 ├── TokenAuthenticator // Token 刷新核心 └── Interceptors // 日志 / Header / 签名等2. 核心设计原则对外暴露 Retrofit 接口隐藏 OkHttp 细节异常统一收敛为自定义异常Token 刷新逻辑对业务完全透明一次配置多处复用二、统一响应模型设计1. 后端通用返回结构约定优于配置{ code: 0, message: success, data: {} }2. 统一 Response DTOdata class ApiResponseT( val code: Int, val message: String, val data: T? )3. Result 包装推荐为了更安全地处理成功 / 失败建议使用Result或sealed class。sealed class ApiResultout T { data class SuccessT(val data: T) : ApiResultT() data class Error( val exception: AppException ) : ApiResultNothing() }三、统一异常处理体系1. 自定义异常基类open class AppException( val code: Int, override val message: String, val cause: Throwable? null ) : RuntimeException(message, cause)2. 异常分类类型说明示例HttpExceptionHTTP 状态码异常404 / 500NetworkException网络不可用SocketTimeoutBusinessException业务错误码code ! 0AuthException认证失败Token 失效class BusinessException(code: Int, message: String) : AppException(code, message) class NetworkException(cause: Throwable) : AppException(-1, 网络连接失败, cause) class AuthException(message: String 登录已过期) : AppException(401, message)3. 统一异常转换器object ExceptionHandler { fun handle(throwable: Throwable): AppException when (throwable) { is HttpException - AppException( throwable.code(), throwable.message() ) is ConnectException, is SocketTimeoutException - NetworkException(throwable) is JsonParseException - AppException( -2, 数据解析失败, throwable ) is AppException - throwable else - AppException( -99, 未知错误, throwable ) } }四、Retrofit CallAdapter统一返回 Result1. 自定义 CallAdapterclass ResultCallAdapterT( private val type: Type ) : CallAdapterT, CallApiResultT { override fun responseType() type override fun adapt(call: CallT): CallApiResultT { return ResultCall(call) } }2. ResultCall 核心逻辑class ResultCallT( private val delegate: CallT ) : CallApiResultT by delegate { override fun enqueue(callback: CallbackApiResultT) { delegate.enqueue(object : CallbackT { override fun onResponse(call: CallT, response: ResponseT) { val body response.body() if (response.isSuccessful body ! null) { val apiResp body as? ApiResponse* if (apiResp?.code 0) { callback.onResponse( thisResultCall, Response.success(ApiResult.Success(body)) ) } else { callback.onResponse( thisResultCall, Response.success( ApiResult.Error( BusinessException( apiResp?.code ?: -1, apiResp?.message ?: 业务异常 ) ) ) ) } } else { callback.onResponse( thisResultCall, Response.success( ApiResult.Error( AppException(response.code(), response.message()) ) ) ) } } override fun onFailure(call: CallT, t: Throwable) { callback.onResponse( thisResultCall, Response.success( ApiResult.Error(ExceptionHandler.handle(t)) ) ) } }) } }五、Token 自动刷新核心难点1. 设计目标✅ Access Token 过期自动刷新✅ 防止并发刷新多个接口同时 401✅ 刷新失败自动登出✅ 刷新成功后重试原请求2. Token 存储抽象interface TokenStore { fun getAccessToken(): String? fun getRefreshToken(): String? fun saveTokens(accessToken: String, refreshToken: String) fun clear() }3. TokenAuthenticator关键class TokenAuthenticator( private val tokenStore: TokenStore ) : Authenticator { Synchronized override fun authenticate( route: Route?, response: Response ): Request? { if (response.code ! 401) return null val refreshToken tokenStore.getRefreshToken() ?: return null // 同步刷新 Token val newToken refreshTokenSynchronously(refreshToken) ?: run { // 刷新失败触发登出 tokenStore.clear() postLogoutEvent() return null } tokenStore.saveTokens( newToken.accessToken, newToken.refreshToken ) // 重试原请求 return response.request.newBuilder() .header(Authorization, Bearer ${newToken.accessToken}) .build() } private fun refreshTokenSynchronously(refreshToken: String): NewToken? { // 使用 OkHttp 同步请求 // 注意不能使用 Retrofit 本身避免递归 } }⚠️关键点Authenticator.authenticate()是在OkHttp 层执行不会走 Retrofit Adapter因此异常不会被二次包装这是它最适合做 Token 刷新的原因。4. 防并发刷新优化锁 标志位Volatile private var isRefreshing false private val lock Any() fun refreshIfNeeded(): Boolean { synchronized(lock) { if (isRefreshing) return false isRefreshing true } try { return doRefresh() } finally { isRefreshing false } }更高级方案可使用CountDownLatch​ 或Flow / SharedFlow​ 通知等待的请求。六、Header 统一注入拦截器class HeaderInterceptor( private val tokenStore: TokenStore ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request chain.request().newBuilder() .addHeader(Content-Type, application/json) .addHeader(Accept, application/json) .apply { tokenStore.getAccessToken()?.let { addHeader(Authorization, Bearer $it) } } .build() return chain.proceed(request) } }七、OkHttp 统一配置object OkHttpProvider { fun create(tokenStore: TokenStore): OkHttpClient { return OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS) .writeTimeout(20, TimeUnit.SECONDS) .addInterceptor(HeaderInterceptor(tokenStore)) .addInterceptor(HttpLoggingInterceptor().apply { level HttpLoggingInterceptor.Level.BODY }) .authenticator(TokenAuthenticator(tokenStore)) .retryOnConnectionFailure(true) .build() } }八、Retrofit 统一入口object RetrofitManager { private lateinit var retrofit: Retrofit fun init(baseUrl: String, tokenStore: TokenStore) { retrofit Retrofit.Builder() .baseUrl(baseUrl) .client(OkHttpProvider.create(tokenStore)) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(ResultCallAdapterFactory()) .build() } fun T create(service: ClassT): T retrofit.create(service) }九、业务层使用示例极其简洁interface UserApi { GET(user/info) fun getUserInfo(): CallApiResultUserInfo } viewModelScope.launch { val result RetrofitManager.create(UserApi::class.java) .getUserInfo() .awaitResult() when (result) { is ApiResult.Success - showUser(result.data) is ApiResult.Error - handleError(result.exception) } }✅业务代码不再关心Token 是否过期是否需要刷新异常如何分类重试逻辑如何实现十、常见问题与优化建议1. RefreshToken 也过期怎么办清除本地登录态发送全局事件EventBus / Flow跳转到登录页记录当前页面登录后恢复2. 多个 401 同时到来如何处理✅ 使用单例刷新 请求队列挂起​✅ 或最简单可靠方案Authenticator synchronized3. 是否可以在拦截器中刷新 Token❌强烈不推荐原因拦截器可能被多次调用容易死循环无法正确处理响应链✅Token 刷新只放在Authenticator4. 日志打印敏感信息Debug 环境打印完整 BodyRelease 环境只打印 URL 状态码Token、Password 字段必须脱敏十一、总结通过本次二次封装我们实现了✅ 统一请求 / 响应模型✅ 统一异常处理HTTP / 业务 / 网络✅ Token 自动刷新无感体验✅ 线程安全 防并发刷新✅ 业务层极度简化一句话总结好的网络层封装应该让业务代码“忘记网络的存在”。