OkHttp拦截器:Android网络请求的「瑞士军刀」

前言

想象一下,你是一个快递站的老板。每天有成千上万的包裹(网络请求)从这里发出,也有无数的包裹(响应)送回来。你需要检查每个包裹是否贴了正确的标签(请求头),记录每个包裹的物流信息(日志),甚至拦截某些包裹进行特殊处理(比如加密或解密)。这时候,你需要的不是一个一个打开包裹检查,而是在包裹的传送带上安装一系列的「关卡」,每个关卡负责特定的任务。

OkHttp的拦截器(Interceptor)就是这样的「关卡」。它允许你在网络请求的生命周期中插入自定义逻辑,比如添加公共请求头、记录请求日志、处理缓存、自动刷新Token等。通过拦截器,你可以在不修改业务代码的情况下,统一处理所有网络请求的通用逻辑,让代码更简洁、可维护性更强。

拦截器的基本概念:应用拦截器 vs 网络拦截器

OkHttp的拦截器分为两种类型:应用拦截器网络拦截器。它们的区别就像快递站的「前台」和「仓库」:

1. 应用拦截器(Application Interceptor)

  • 位置:在快递站的前台,负责处理用户直接提交的包裹。
  • 特点
    • 不关心网络:不管包裹是从网络发送还是从缓存读取,应用拦截器都会被调用。
    • 不处理重定向和重试:如果包裹需要重定向(比如302响应)或重试,应用拦截器不会感知到这些过程。
    • 只调用一次:每个请求只会调用一次应用拦截器,即使请求被重试多次。
  • 使用场景:添加公共请求头、记录全局日志、处理响应数据(如解密)等。

2. 网络拦截器(Network Interceptor)

  • 位置:在快递站的仓库,负责处理实际发送到网络的包裹。
  • 特点
    • 关心网络:只有当包裹真正通过网络发送时,网络拦截器才会被调用。如果包裹来自缓存,网络拦截器不会执行。
    • 处理重定向和重试:网络拦截器会感知到请求的重定向和重试过程,并且会在每次网络请求时被调用。
    • 可访问原始数据:可以获取到请求和响应的原始字节流,适合处理网络层的细节,比如监控流量、修改原始数据等。
  • 使用场景:监控网络请求的实际传输过程、处理网络层的重试逻辑、修改原始请求体等。

自定义拦截器的实现:从Hello World开始

现在,我们来实现一个简单的日志拦截器,记录每个请求的URL、请求头和响应状态码。

1. 创建拦截器类

kotlin 复制代码
class LoggingInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val url = request.url.toString()
        val method = request.method
        val headers = request.headers.toMultimap()

        // 打印请求信息
        println("🌏 Request: $method $url")
        println("📦 Headers: $headers")

        // 执行请求
        val response = chain.proceed(request)

        // 打印响应信息
        val statusCode = response.code
        println("🚀 Response: $statusCode from $url")

        return response
    }
}

2. 将拦截器添加到OkHttpClient

kotlin 复制代码
val client = OkHttpClient.Builder()
    .addInterceptor(LoggingInterceptor()) // 添加应用拦截器
    .build()

3. 代码解释

  • chain.proceed(request):这是拦截器链的核心方法。它会将当前请求传递给下一个拦截器,并返回最终的响应。如果不调用这个方法,请求链就会中断,不会发送到服务器。
  • 请求处理 :在调用chain.proceed(request)之前,我们可以修改请求(如添加请求头),或者直接返回一个伪造的响应。
  • 响应处理 :在调用chain.proceed(request)之后,我们可以处理响应(如解密数据),或者修改响应后返回。

应用拦截器 vs 网络拦截器:详细对比

为了让你更清楚地理解两者的区别,我们用一个表格来总结:

特性 应用拦截器 网络拦截器
调用时机 所有请求(包括缓存)都会调用 只有实际发送到网络的请求才会调用
重定向/重试 不感知,只处理原始请求 感知,可以处理重定向和重试后的请求
调用次数 每个请求调用一次 每次网络请求调用一次(可能多次)
原始数据访问 无法获取原始字节流 可以获取请求和响应的原始字节流
最佳实践 处理全局逻辑(如添加公共头、日志) 处理网络层细节(如监控流量、修改原始数据)

举个🌰:假设你需要记录所有请求的日志,不管是来自网络还是缓存,那么应用拦截器是更好的选择。如果你需要监控每个网络请求的实际传输时间,那么网络拦截器更合适。

实际应用场景:拦截器的「十八般武艺」

拦截器的应用场景非常广泛,下面我们列举几个常见的例子:

1. 日志记录:监控网络请求的「天眼」

日志拦截器是最常见的拦截器之一。它可以记录请求的URL、方法、请求头、响应状态码等信息,帮助你快速定位网络问题。

kotlin 复制代码
class LoggingInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val url = request.url.toString()
        val method = request.method
        val headers = request.headers.toMultimap()

        // 打印请求信息
        println("🌏 Request: $method $url")
        println("📦 Headers: $headers")

        // 执行请求
        val response = chain.proceed(request)

        // 打印响应信息
        val statusCode = response.code
        println("🚀 Response: $statusCode from $url")

        return response
    }
}

2. 缓存处理:让请求「飞」得更快

通过缓存拦截器,你可以将常用的响应缓存到本地,减少网络请求的次数,提升应用性能。

kotlin 复制代码
class CacheInterceptor(private val cacheSize: Long) : Interceptor {
    private val cache = Cache(File(context.cacheDir, "okhttp_cache"), cacheSize)

    override fun intercept(chain: Interceptor.Chain): Response {
        // 检查缓存
        val cachedResponse = cache.get(chain.request())
        if (cachedResponse != null) {
            println("📤 Using cached response")
            return cachedResponse
        }

        // 发起网络请求
        val response = chain.proceed(chain.request())

        // 缓存响应
        cache.put(response.newBuilder().build())
        println("📥 Cached response")

        return response
    }
}

3. Token自动刷新:避免用户频繁登录

当Token过期时,拦截器可以自动刷新Token,并重新发起请求,让用户无感知。

kotlin 复制代码
class TokenRefreshInterceptor(private val tokenManager: TokenManager) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        var response = chain.proceed(request)

        // 如果Token过期,刷新Token并重新请求
        if (response.code == 401) {
            tokenManager.refreshToken()
            request = request.newBuilder()
                .header("Authorization", "Bearer ${tokenManager.getToken()}")
                .build()
            response = chain.proceed(request)
        }

        return response
    }
}

4. 参数加密:让数据传输更安全

拦截器可以对请求参数进行加密,防止数据在传输过程中被窃取。

kotlin 复制代码
class EncryptionInterceptor(private val encryptor: Encryptor) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val body = request.body

        // 加密请求体
        if (body != null) {
            val encryptedBody = encryptor.encrypt(body.toString())
            val newBody = RequestBody.create(MediaType.parse("application/json"), encryptedBody)
            request = request.newBuilder()
                .post(newBody)
                .build()
        }

        return chain.proceed(request)
    }
}

高级技巧:拦截器的「进阶玩法」

1. 动态域名切换:给快递换地址标签(含HTTPS证书兼容)

在多环境部署(如开发、测试、生产)或灰度发布场景中,我们需要动态切换服务器域名。这时,拦截器就像一个「地址标签更换器」,在请求发出前自动替换域名。不过,HTTPS证书的兼容性问题可能会让你头疼------这就好比给快递换地址时,还得确保新地址的门锁能匹配你的钥匙。

实战案例:多环境动态切换

kotlin 复制代码
class DynamicDomainInterceptor(private val domainManager: DynamicDomainManager) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val newUrl = originalRequest.url.newBuilder()
            .host(domainManager.currentDomain) // 替换域名
            .build()
        
        val newRequest = originalRequest.newBuilder()
            .url(newUrl)
            .build()
        
        return chain.proceed(newRequest)
    }
}

// 动态域名管理类(线程安全)
object DynamicDomainManager {
    private var currentDomain: String = "https://prod.api.com" // 默认生产环境
    private val lock = Any()

    fun updateDomain(newDomain: String) {
        synchronized(lock) {
            currentDomain = newDomain
            // 可选:持久化到SharedPreferences或数据库
        }
    }

    val currentDomain: String
        get() = synchronized(lock) { currentDomain }
}

HTTPS证书兼容方案

如果不同域名使用不同证书,你需要配置OkHttp的SSLContext。例如,为测试环境添加自签名证书:

kotlin 复制代码
// 加载证书(放在assets目录)
val certificate = context.assets.open("test_cert.crt").use { inputStream ->
    CertificateFactory.getInstance("X.509").generateCertificate(inputStream) as X509Certificate
}

// 创建自定义TrustManager
val trustManager = object : X509TrustManager {
    override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
    override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
        chain.forEach { it.checkValidity() } // 验证证书有效期
    }
    override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf(certificate)
}

// 配置OkHttpClient
val client = OkHttpClient.Builder()
    .sslSocketFactory(SSLContext.getInstance("TLS").apply {
        init(null, arrayOf(trustManager), null)
    }.socketFactory, trustManager)
    .addInterceptor(DynamicDomainInterceptor(DynamicDomainManager))
    .build()

实现原理

  • 动态域名管理类:通过单例和锁机制保证线程安全,支持实时更新域名。
  • 拦截器替换逻辑 :在intercept方法中,使用newBuilder()修改请求的host字段。
  • HTTPS证书兼容 :通过自定义TrustManager信任特定证书,避免SSLHandshakeException

2. 重试机制:给网络请求上「保险绳」(协程版)

网络波动时,自动重试可以大幅提升请求成功率。拦截器能像「保险绳」一样,在请求失败时自动重试。这里我们用协程实现非阻塞的指数退避重试。

带指数退避的协程重试拦截器

kotlin 复制代码
class CoroutineRetryInterceptor(
    private val maxRetries: Int = 3,
    private val baseDelayMs: Long = 1000
) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        var retries = 0

        while (retries < maxRetries) {
            try {
                return chain.proceed(request)
            } catch (e: IOException) {
                retries++
                if (retries > maxRetries) throw e

                // 协程中使用delay,避免阻塞线程
                withContext(Dispatchers.IO) {
                    delay(baseDelayMs * (2.0.pow(retries - 1).toLong()))
                }
            }
        }
        throw IOException("Max retries exceeded")
    }
}

核心策略

  • 指数退避:重试间隔随失败次数翻倍(如1s → 2s → 4s),避免服务器被频繁重试压垮。

  • 协程优势 :使用Dispatchers.IO在后台线程等待,不影响主线程。

  • 状态码过滤 :可扩展为仅对特定状态码(如500、503)重试:

    kotlin 复制代码
    if (response.code !in 200..299) {
        retries++
    }

3. 性能监控:给网络请求做「体检」(含数据可视化)

拦截器能像「网络医生」一样,记录请求耗时、流量、状态码等关键指标,帮助优化性能。数据记录后,你可以用开源库(如OkLog)生成可视化报告。

性能监控拦截器

kotlin 复制代码
class PerformanceInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val startNs = System.nanoTime()
        val request = chain.request()
        val response = chain.proceed(request)
        val durationMs = (System.nanoTime() - startNs) / 1_000_000

        // 记录关键指标
        val metrics = mapOf(
            "url" to request.url.toString(),
            "method" to request.method,
            "duration_ms" to durationMs,
            "status_code" to response.code,
            "size_bytes" to response.body?.contentLength() ?: 0
        )
        
        // 使用OkLog发送数据到Chrome开发者工具
        OkLog.log(metrics)
        
        return response
    }
}

数据应用

  • 请求耗时分析:找出慢接口,优化后端或增加缓存。
  • 流量监控:防止大文件传输导致用户流量超限。
  • 可视化工具
    • OkLog:在日志中生成可点击的链接,直接查看响应详情。
    • Monitor:在应用内显示实时网络数据面板。

4. 拦截器链控制:掌控请求「流水线」(含Stetho集成)

通过拦截器可以灵活控制请求链的执行,甚至提前终止流程。这在调试时尤其有用,比如模拟响应。

模拟响应拦截器(调试神器)

kotlin 复制代码
class MockInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        if (request.url.pathSegments.contains("mock")) {
            // 返回模拟数据,跳过网络请求
            return Response.Builder()
                .request(request)
                .code(200)
                .body(ResponseBody.create(MediaType.parse("application/json"), """
                    {
                        "mock": "data",
                        "message": "This is a mock response"
                    }
                """.trimIndent()))
                .build()
        }
        return chain.proceed(request)
    }
}

调试技巧

  • 与Stetho集成 :在Chrome开发者工具中查看模拟响应:

    kotlin 复制代码
    OkHttpClient.Builder()
        .addNetworkInterceptor(StethoInterceptor()) // 抓包工具
        .addInterceptor(MockInterceptor())
        .build()
  • 动态数据:从本地文件或数据库读取模拟数据,支持不同场景。

5. 数据解密:给敏感数据「剥壳」(AES加密实战)

如果服务器返回的数据是加密的(如AES加密),可以用拦截器自动解密,避免在每个接口回调中重复处理。

AES解密拦截器

kotlin 复制代码
class DecryptInterceptor(private val decryptor: Decryptor) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val response = chain.proceed(chain.request())
        val encryptedBody = response.body?.string() ?: return response

        // 解密响应体
        val decryptedBody = decryptor.aesDecrypt(encryptedBody)

        // 构建新的响应体
        val newBody = ResponseBody.create(
            response.body?.contentType(),
            decryptedBody
        )

        return response.newBuilder()
            .body(newBody)
            .build()
    }
}

// 解密接口
interface Decryptor {
    fun aesDecrypt(encryptedData: String): String
}

// 实现示例
class AesDecryptor(private val key: String) : Decryptor {
    override fun aesDecrypt(encryptedData: String): String {
        // 使用AES算法解密
        val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
        cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key.toByteArray(), "AES"))
        return String(cipher.doFinal(Base64.getDecoder().decode(encryptedData)))
    }
}

安全注意事项

  • 密钥管理:避免硬编码密钥,可通过服务端动态下发。
  • 加密算法:优先使用更安全的模式(如AES/GCM)。
  • 性能优化:解密操作可能耗时,建议在网络拦截器中使用(不处理缓存)。

6. 流量控制:给请求「限速」(信号量实现)

在弱网环境下,过多的并发请求会导致网络拥堵。拦截器可以实现简单的流量控制,限制同时发起的请求数量。

流量控制拦截器

kotlin 复制代码
class TrafficControlInterceptor(
    private val maxConcurrentRequests: Int = 3 // 最大并发请求数
) : Interceptor {
    private val semaphore = Semaphore(maxConcurrentRequests) // 信号量控制并发

    override fun intercept(chain: Interceptor.Chain): Response {
        semaphore.acquire() // 获取许可,若已满则阻塞等待
        return try {
            chain.proceed(chain.request())
        } finally {
            semaphore.release() // 释放许可
        }
    }
}

适用场景

  • 列表页快速滑动:限制同时加载的图片或数据请求。

  • 启动优化:避免App启动时大量初始化请求瞬间涌入网络。

  • 与Dispatcher结合

    kotlin 复制代码
    OkHttpClient.Builder()
        .dispatcher(Dispatcher().apply {
            maxRequests = 5 // 全局并发限制
        })
        .addInterceptor(TrafficControlInterceptor(3)) // 接口级并发限制
        .build()

7. 线程安全设计:给拦截器上「安全锁」(Token管理优化)

多线程环境下,拦截器可能被并发调用,需确保线程安全。例如,Token刷新逻辑需要原子操作。

线程安全的Token管理器

kotlin 复制代码
class TokenManager {
    private val currentToken = AtomicReference<String>("")

    suspend fun fetchToken(): String {
        return withContext(Dispatchers.IO) {
            // 异步请求刷新Token
            val response = apiService.refreshToken()
            currentToken.set(response.token)
            response.token
        }
    }

    val token: String
        get() = currentToken.get()
}

安全策略

  • 原子操作 :使用AtomicReference保证Token的线程安全访问。

  • 协程同步 :在协程中使用synchronized块:

    kotlin 复制代码
    private val lock = Any()
    suspend fun refreshToken() {
        synchronized(lock) {
            // 刷新Token的逻辑
        }
    }

8. 动态请求头注入:按需贴「特殊标签」

根据不同请求动态添加请求头,例如用户权限或设备信息。

动态请求头拦截器

kotlin 复制代码
class DynamicHeaderInterceptor(private val headerProvider: HeaderProvider) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val headers = headerProvider.getHeaders(request.url)
        
        val newRequest = request.newBuilder()
            .headers(headers)
            .build()
        
        return chain.proceed(newRequest)
    }
}

// 头信息提供器示例
class PathBasedHeaderProvider : HeaderProvider {
    override fun getHeaders(url: HttpUrl): Headers {
        return if (url.pathSegments.contains("admin")) {
            Headers.of("X-Permission", "admin")
        } else {
            Headers.of("X-Permission", "user")
        }
    }
}

应用场景

  • 权限控制:根据用户角色动态添加权限头。
  • A/B测试:为不同用户群体注入差异化头信息。
  • 设备标识:自动添加设备型号、系统版本等头。

拦截器链的「黑科技」

1. 拦截器顺序的「蝴蝶效应」

拦截器的添加顺序会影响执行逻辑。例如:

kotlin 复制代码
OkHttpClient.Builder()
    .addInterceptor(TokenRefreshInterceptor()) // 先刷新Token
    .addInterceptor(RetryInterceptor()) // 再重试
    .addInterceptor(PerformanceInterceptor()) // 最后记录性能
    .build()
  • TokenRefreshInterceptor在请求前添加Authorization头。
  • RetryInterceptor在Token过期时重试请求。
  • PerformanceInterceptor记录整个流程的耗时。

2. 拦截器链的「短路」

拦截器可以提前返回响应,终止后续拦截器的执行。例如:

kotlin 复制代码
class AuthInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        if (request.headers["Authorization"] == null) {
            // 返回401未授权响应,跳过后续拦截器
            return Response.Builder()
                .request(request)
                .code(401)
                .body(ResponseBody.create(null, "Unauthorized"))
                .build()
        }
        return chain.proceed(request)
    }
}

总结

OkHttp拦截器就像一个「瑞士军刀」,可以灵活应对各种网络场景:

  • 动态域名切换:应对多环境部署,含HTTPS证书兼容。
  • 重试机制:网络波动时自动重试,协程版避免阻塞。
  • 性能监控:记录关键指标,结合可视化工具优化性能。
  • 模拟响应:调试神器,快速验证接口逻辑。
  • 数据解密:处理加密响应,保护敏感数据。
  • 流量控制:限制并发请求,避免网络拥堵。
  • 线程安全设计:确保多线程环境下的稳定性。

通过合理组合这些技巧,你可以构建出健壮、高效的网络层架构。拦截器的强大之处在于它的「无侵入性」------无需修改原有代码,就能为整个网络请求流程注入新功能。现在,拿起你的「瑞士军刀」,在Android开发中尽情发挥吧!

相关推荐
用户207038619497 小时前
Android AutoService 解耦实战
android
就是帅我不改7 小时前
震惊!高并发下,我竟用Spring普通Bean实现了线程级隔离!
后端·面试·github
jctech7 小时前
解构ComboLite:0 Hook的背后,是哪些精妙的架构设计?
android
jctech7 小时前
从0到1,用`ComboLite`构建一个“万物皆可插拔”的动态化App
android
jctech7 小时前
你的App越来越“胖”了吗?给Android应用“减肥”的终极秘诀——插件化
android
Alporal7 小时前
前端小白误闯天家:项目选什么?该不该往新技术方向包装
面试
jctech7 小时前
告别Hook!ComboLite:为Jetpack Compose而生的下一代Android插件化框架
android
TimelessHaze7 小时前
面试必备:深入理解与实现高效瀑布流布局
前端·面试·trae
掘金安东尼8 小时前
前端周刊第429期(2025年8月25日–8月31日)
前端·javascript·面试