拦截器机制
问题
OkHttp 拦截器的设计原理和常见应用场景有哪些?
答案
拦截器链执行顺序
自定义拦截器的基本结构
class MyInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
// 1. 获取原始请求
val request = chain.request()
// 2. 修改请求(可选)
val newRequest = request.newBuilder()
.addHeader("Custom-Header", "value")
.build()
// 3. 传递给下一个拦截器
val response = chain.proceed(newRequest)
// 4. 修改响应(可选)
return response.newBuilder()
.addHeader("Response-Header", "value")
.build()
}
}
常见拦截器实现
1. 日志拦截器
class LoggingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val startTime = System.nanoTime()
Log.d("HTTP", "--> ${request.method} ${request.url}")
request.headers.forEach { (name, value) ->
Log.d("HTTP", "$name: $value")
}
val response = chain.proceed(request)
val duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)
Log.d("HTTP", "<-- ${response.code} ${request.url} (${duration}ms)")
return response
}
}
推荐使用 HttpLoggingInterceptor
OkHttp 官方提供了 okhttp3.logging.HttpLoggingInterceptor,支持 4 种日志级别:NONE、BASIC、HEADERS、BODY。
2. 认证 Token 拦截器
class AuthInterceptor(
private val tokenProvider: TokenProvider
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenProvider.getAccessToken()
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(request)
}
}
3. Token 自动刷新拦截器
class TokenRefreshInterceptor(
private val tokenProvider: TokenProvider
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
// 401 表示 Token 过期
if (response.code == 401) {
// 同步刷新 Token(加锁避免并发刷新)
synchronized(this) {
val newToken = tokenProvider.refreshToken()
if (newToken != null) {
// 关闭旧响应
response.close()
// 使用新 Token 重试请求
val newRequest = request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
return chain.proceed(newRequest)
}
}
}
return response
}
}
4. 公共参数拦截器
class CommonParamsInterceptor(
private val appVersion: String,
private val deviceId: String
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
// 为 GET 请求添加 Query 参数
val url = original.url.newBuilder()
.addQueryParameter("app_version", appVersion)
.addQueryParameter("device_id", deviceId)
.addQueryParameter("platform", "android")
.build()
val request = original.newBuilder()
.url(url)
.build()
return chain.proceed(request)
}
}
5. 缓存策略拦截器
// 强制使用缓存(无网络时)
class ForceCacheInterceptor(
private val context: Context
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (!isNetworkAvailable(context)) {
// 无网络时强制使用缓存
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build()
}
return chain.proceed(request)
}
private fun isNetworkAvailable(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE)
as ConnectivityManager
val network = cm.activeNetwork ?: return false
val capabilities = cm.getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(
NetworkCapabilities.NET_CAPABILITY_INTERNET
)
}
}
6. 重试拦截器
class RetryInterceptor(
private val maxRetries: Int = 3
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var response: Response? = null
var exception: IOException? = null
for (attempt in 0..maxRetries) {
try {
response?.close()
response = chain.proceed(request)
if (response.isSuccessful) return response
} catch (e: IOException) {
exception = e
if (attempt < maxRetries) {
// 指数退避
val delay = (2.0.pow(attempt) * 1000).toLong()
Thread.sleep(delay)
}
}
}
throw exception ?: IOException("Unknown error after $maxRetries retries")
}
}
拦截器注册顺序建议
val client = OkHttpClient.Builder()
// Application Interceptors(按顺序执行)
.addInterceptor(CommonParamsInterceptor(...)) // 1. 公共参数
.addInterceptor(AuthInterceptor(...)) // 2. 认证
.addInterceptor(LoggingInterceptor()) // 3. 日志(放最后看完整请求)
.addInterceptor(TokenRefreshInterceptor(...)) // 4. Token 刷新
// Network Interceptors
.addNetworkInterceptor(ForceCacheInterceptor(...)) // 缓存控制
.build()
常见面试问题
Q1: Application Interceptor 和 Network Interceptor 的执行时机有何不同?
答案:
- Application Interceptor:在所有内置拦截器之前执行。每次调用
execute()/enqueue()只执行一次,即使发生重定向或重试也不会重复执行。看到的是应用层的原始请求。 - Network Interceptor:在
ConnectInterceptor和CallServerInterceptor之间执行。看到的是经过 Bridge 补充 Headers 后的完整请求。如果发生重定向,会执行多次。缓存命中时不执行。
Q2: 如何实现请求的统一错误处理?
答案:
通过 Application Interceptor 拦截所有响应,根据 HTTP 状态码和业务错误码做统一处理:
class ErrorHandleInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
when (response.code) {
401 -> { /* Token 过期,跳转登录 */ }
403 -> { /* 权限不足 */ }
in 500..599 -> { /* 服务器异常 */ }
}
return response
}
}
Q3: 拦截器中如何避免多线程问题?
答案:
- 拦截器实例被多线程共享:不要在拦截器中使用可变成员变量
- Token 刷新场景:使用
synchronized或其他同步机制保证只有一个线程执行刷新 - Response Body 只能读一次:如果在拦截器中读取了 Body(如日志),必须重新创建
ResponseBody