OkHttp 拦截器:自定义网络日志、缓存与重试

FreeGuideOnline 最新 2026-06-17

初识 OkHttp 拦截器

拦截器是 OkHttp 中非常强大的功能,它允许你监控、修改和重试 HTTP 调用。无论是添加统一的请求头、记录网络日志、实现缓存策略还是自动重试失败请求,都可以通过拦截器优雅地完成,无需在业务代码中到处重复逻辑。

OkHttp 的拦截器分为两种:

  • 应用拦截器(Application Interceptors):通过 addInterceptor() 添加,作用于最顶层,能拦截最终发出的请求和最初返回的响应,不会重定向和重试影响。
  • 网络拦截器(Network Interceptors):通过 addNetworkInterceptor() 添加,更接近网络层,能捕获到重定向和重试等中间过程。

理解它们的区别后,我们将通过三个典型场景手把手教你自定义拦截器:记录网络日志、实现透明缓存、构建失败重试机制。

环境准备

确保你的项目已添加 OkHttp 依赖。以 Gradle 为例:

implementation("com.squareup.okhttp3:okhttp:4.12.0")

创建 OkHttpClient 实例,并添加我们的自定义拦截器:

val client = OkHttpClient.Builder()
    .addInterceptor(/* 应用拦截器 */)
    .addNetworkInterceptor(/* 网络拦截器 */)
    .build()

下面我们逐步实现三种常用拦截器。

自定义网络日志拦截器

很多时候需要查看请求的详细信息和响应时间,用来调试接口。我们创建一个 LoggingInterceptor 来打印完整的请求行、请求头和请求体(如果安全),以及响应码、响应头和响应体。

拦截器实现

import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.internal.http.promisesBody
import okio.Buffer
import java.io.IOException
import java.nio.charset.Charset

class LoggingInterceptor : Interceptor {

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val startTime = System.nanoTime()

        // 打印请求信息
        println("--> ${request.method} ${request.url} HTTP/1.1")
        if (request.headers.size > 0) {
            println("Headers: ${request.headers}")
        }
        if (request.body != null && request.body!!.contentType() != null) {
            println("Content-Type: ${request.body!!.contentType()}")
        }
        if (request.body != null && request.body!!.contentLength() != -1L) {
            println("Content-Length: ${request.body!!.contentLength()}")
        }

        // 可选:打印请求体(限于非加密或文本类型)
        request.body?.let { body ->
            if (body.contentType()?.let { it.type == "text" || it.subtype == "json" || it.subtype == "x-www-form-urlencoded" } == true) {
                val buffer = Buffer()
                body.writeTo(buffer)
                val charset = body.contentType()?.charset(Charset.forName("UTF-8")) ?: Charset.forName("UTF-8")
                println("Body: ${buffer.readString(charset)}")
                buffer.close()
            }
        }

        println("--> END ${request.method}")

        val response: Response
        try {
            response = chain.proceed(request)
        } catch (e: Exception) {
            println("<-- HTTP FAILED: $e")
            throw e
        }

        val tookMs = (System.nanoTime() - startTime) / 1_000_000
        println("<-- ${response.code} ${request.url} (${tookMs}ms)")
        if (response.headers.size > 0) {
            println("Headers: ${response.headers}")
        }
        response.body?.let { body ->
            if (body.contentType() != null) {
                println("Content-Type: ${body.contentType()}")
            }
            if (body.contentLength() != -1L) {
                println("Content-Length: ${body.contentLength()}")
            }
            // 打印响应体(同样只处理文本类)
            if (body.contentType()?.let { it.type == "text" || it.subtype == "json" } == true) {
                val source = body.source()
                source.request(Long.MAX_VALUE)
                val buffer = source.buffer
                val charset = body.contentType()?.charset(Charset.forName("UTF-8")) ?: Charset.forName("UTF-8")
                if (buffer.size > 0) {
                    println("Body: ${buffer.clone().readString(charset)}")
                }
            }
        }
        println("<-- END HTTP")

        return response
    }
}

添加到客户端

建议使用应用拦截器,这样即使发生重定向,也能看到最初的请求和最终的响应。或者同时添加网络拦截器观察中间步骤。

val client = OkHttpClient.Builder()
    .addInterceptor(LoggingInterceptor())
    .build()

运行后,你将在控制台看到类似这样的输出:

--> GET https://httpbin.org/get HTTP/1.1
Headers: Connection: Keep-Alive, User-Agent: okhttp/4.12.0
--> END GET
<-- 200 https://httpbin.org/get (152ms)
Headers: Content-Type: application/json, Content-Length: 255
Body: { "args": {}, ... }
<-- END HTTP

缓存拦截器

OkHttp 本身支持 HTTP 缓存,但有时我们需要更灵活的控制,比如强制使用缓存一定时间、离线时使用缓存数据,或者根据业务逻辑自定义缓存键。这里我们通过拦截器实现一个简单但实用的内存缓存策略。

设计思路

  • 使用 LruCache(或 Cache 接口,这里为演示用内存缓存)保存请求的响应。
  • 拦截器的 intercept 方法中,先用请求的 urlmethod 构建缓存键。
  • 若缓存存在且未过期,直接返回缓存的响应。
  • 若不存在或过期,则执行网络请求,将响应存入缓存后再返回。
  • 可以添加 Cache-Control 头让 OkHttp 内置缓存来配合,但这里完全自定义演示。

拦截器实现

import okhttp3.*
import okhttp3.internal.cache.CacheStrategy
import okhttp3.internal.cache.InternalCache
import okio.Buffer
import java.io.IOException

class ForceCacheInterceptor(private val maxAgeSeconds: Int) : Interceptor {

    private val cache = object : LinkedHashMap<String, CachedResponse>() {
        override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, CachedResponse>?): Boolean {
            // 保持缓存数量,防止内存溢出
            return size > 100
        }
    }

    data class CachedResponse(val response: Response, val timestamp: Long)

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val cacheKey = "${request.method}:${request.url}"

        // 检查缓存
        cache[cacheKey]?.let { cached ->
            val ageSeconds = (System.currentTimeMillis() - cached.timestamp) / 1000
            if (ageSeconds < maxAgeSeconds) {
                // 缓存未过期,直接返回
                println("Cache hit for $cacheKey (age ${ageSeconds}s)")
                return cached.response.newBuilder()
                    .removeHeader("Content-Encoding") // 防止双重编码
                    .build()
            } else {
                // 过期移除
                cache.remove(cacheKey)
            }
        }

        // 执行网络请求
        val networkResponse = chain.proceed(request)

        // 缓存可缓存的响应(状态码 200 且方法为 GET)
        if (request.method == "GET" && networkResponse.code == 200) {
            val body = networkResponse.body ?: return networkResponse
            val bodyBytes = body.bytes() // 读取响应体,注意会消耗原始body
            val newBody = ResponseBody.create(body.contentType(), bodyBytes)
            val cachedResponse = networkResponse.newBuilder()
                .body(newBody)
                .build()
            cache[cacheKey] = CachedResponse(cachedResponse, System.currentTimeMillis())
            println("Cached response for $cacheKey")
            return networkResponse.newBuilder()
                .body(ResponseBody.create(body.contentType(), bodyBytes))
                .build()
        }

        return networkResponse
    }
}

使用示例

val client = OkHttpClient.Builder()
    .addInterceptor(ForceCacheInterceptor(maxAgeSeconds = 60)) // 60 秒内复用缓存
    .build()

// 第一次请求会访问网络
val response1 = client.newCall(Request.Builder().url("https://httpbin.org/get").build()).execute()
// 第二次立即请求会命中缓存
val response2 = client.newCall(Request.Builder().url("https://httpbin.org/get").build()).execute()

对于更正式的生产环境,推荐使用 OkHttp 自带的 Cache 类并设置缓存目录,此拦截器更适合演示自定义逻辑。

失败重试拦截器

网络波动时,自动重试请求可以大大提高成功率。我们需要小心处理重试逻辑,避免对非幂等请求(如 POST)盲目重试。

设计重试规则

  • 只重试 GET 请求(幂等)。
  • 遇到网络异常(如 IOException)或服务器返回 5xx 错误时重试。
  • 设置最大重试次数和重试间隔(指数退避)。

拦截器实现

import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
import kotlin.math.pow

class RetryInterceptor(private val maxRetry: Int = 3) : Interceptor {

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        var tryCount = 0
        var lastException: IOException? = null
        var response: Response? = null

        while (tryCount <= maxRetry) {
            if (tryCount > 0) {
                // 指数退避等待:1s, 2s, 4s...
                val waitSeconds = 2.0.pow(tryCount - 1).toLong() * 1000L
                println("Retry ($tryCount/$maxRetry) after ${waitSeconds}ms for ${request.url}")
                Thread.sleep(waitSeconds)
            }

            try {
                // 如果是重试,需要重新创建 network 层面的请求?这里直接 proceed
                response = chain.proceed(request)
                if (response.isSuccessful || request.method != "GET" || tryCount >= maxRetry) {
                    // 成功或非 GET 请求或达到最大次数终止重试
                    return response
                }
                // 如果是 5xx 错误并且是 GET 请求,继续重试
                if (response.code in 500..599) {
                    println("Retrying due to server error ${response.code}")
                    response.close()
                    tryCount++
                    continue
                }
                // 其他情况直接返回
                return response
            } catch (e: IOException) {
                println("IOException: ${e.message}")
                if (request.method != "GET" || tryCount >= maxRetry) {
                    throw e
                }
                lastException = e
                tryCount++
            }
        }

        // 如果所有重试都失败,抛出最后遇到的异常或返回最后的响应
        if (lastException != null) throw lastException
        return response!! // 理论上不会到这里
    }
}

添加拦截器

推荐添加为应用拦截器,这样重试逻辑对所有的网络交互都生效。同时注意不要和内置自动重连混淆。

val client = OkHttpClient.Builder()
    .addInterceptor(RetryInterceptor(maxRetry = 3))
    .build()

测试效果

你可以模拟一个不稳定的端点,或使用 httpbin.org/status/500 观察重试日志:

--> GET https://httpbin.org/status/500
Retrying due to server error 500
Retry (1/3) after 1000ms for https://httpbin.org/status/500
Retrying due to server error 500
Retry (2/3) after 2000ms for ...

这样即使接口偶发故障,也能通过自动重试提高用户体验。

拦截器组合与注意事项

在实际项目中,你可能会将多个拦截器搭配使用:

val client = OkHttpClient.Builder()
    .addInterceptor(LoggingInterceptor())
    .addInterceptor(RetryInterceptor())
    .addInterceptor(ForceCacheInterceptor(60))
    .build()
  • 执行顺序:应用拦截器按添加顺序依次执行,每个拦截器的 interceptchain.proceed() 会调用下一个拦截器,最终到达网络。网络拦截器同理。
  • 不要消耗响应体:如果拦截器读取了响应体用于日志或缓存,必须用 peekBody 或复制一份新的 ResponseBody,否则下游将无法读取。
  • 线程安全:拦截器可能在多线程中执行,共享可变状态时要保证线程安全(如使用同步集合)。
  • 重试与缓存冲突:重试拦截器最好放在缓存拦截器下方(即更早添加),这样缓存命中后不会触发重试逻辑。

小结

通过自定义拦截器,你可以在 OkHttp 的请求-响应链上注入强大而统一的横切关注点:

  • 日志拦截器提供了全透明的网络调试能力。
  • 缓存拦截器实现了高效的本地数据复用,减少网络消耗。
  • 重试拦截器增强了应用的健壮性,优雅处理短暂故障。

这只是拦截器能力的冰山一角。你可以扩展实现签名认证、动态域名替换、请求加密等更高级的功能。掌握拦截器,你就能真正掌控 OkHttp 的每一次网络交互。