OkHttp 拦截器深入解析:Application 与 Network 拦截器的机制、链路与源码实现
【免费下载链接】okhttpA meticulous HTTP client for the JVM, Android, and GraalVM.项目地址: https://gitcode.com/gh_mirrors/okh/okhttp
本文围绕 OkHttp 的拦截器(Interceptor)机制展开:先讲清楚Interceptor与Chain的核心契约,再对照 官方文档 中的应用/网络拦截器对照实验,最后深入 RealCall、RealInterceptorChain 源码,说明拦截器链的完整组装顺序、chain.proceed()的约束校验,以及请求/响应改写的实战写法。读完后,你将能独立编写日志、压缩、缓存头等拦截器,并准确判断某个逻辑该放在 Application 层还是 Network 层。
一、Interceptor 与 Chain:拦截器契约
OkHttp 把每个调用抽象为一条拦截器链。官方文档给出的最小示例是一个日志拦截器:在proceed()前后分别记录请求与响应,并测量耗时:
class LoggingInterceptor implements Interceptor { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request request = chain.request(); long t1 = System.nanoTime(); logger.info(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers())); Response response = chain.proceed(request); long t2 = System.nanoTime(); logger.info(String.format("Received response for %s in %.1fms%n%s", response.request().url(), (t2 - t1) / 1e6d, response.headers())); return response; } }对chain.proceed(request)的调用是每个拦截器实现的关键。这个看似简单的调用,正是所有 HTTP 工作发生的地方——它产生最终满足请求的响应。文档特别强调:如果对chain.proceed(request)的调用不止一次,则之前的响应体必须先关闭。
Interceptor 接口的完整定义
在 Interceptor.kt 中,Interceptor被声明为 Kotlin 的fun interface,只有一个方法intercept(chain: Chain): Response,并且提供了紧凑的 lambda 语法:
fun interface Interceptor { @Throws(IOException::class) fun intercept(chain: Chain): Response companion object { inline operator fun invoke(crossinline block: (chain: Chain) -> Response): Interceptor = Interceptor { block(it) } } }接口 KDoc 还明确了异常语义,这是文档没有展开、但对实现者很重要的部分:
- 抛
IOException表示连接类失败(包括服务器不可达等自然异常,以及合成异常); - 抛其他类型的异常会取消当前调用:同步调用(
Call.execute)中异常直接传播给调用方;异步调用(Call.enqueue)中会向调用方传播一个IOException,而拦截器本身的异常会交给当前线程的未捕获异常处理器——在 Android 上默认会导致应用崩溃; - 推荐用合成 HTTP 响应来优雅地失败,而不是抛非 IO 异常。
Chain接口(Interceptor.kt L84-L297)暴露的能力远不止request()和proceed(),包括:
| 方法 | 说明 | 使用限制 |
|---|---|---|
connection(): Connection? | 返回本次请求所用的连接 | 仅网络拦截器非 null;应用拦截器中恒为 null |
call(): Call | 返回所属的 Call | — |
withConnectTimeout / withReadTimeout / withWriteTimeout | 调整本次调用的超时 | 仅应用拦截器(见下文源码说明) |
withDns / withCache / withProxy / withAuthenticator / withCookieJar | 覆盖单个调用的 DNS、缓存、代理、认证器、CookieJar | 仅应用拦截器 |
withSslSocketFactory / withHostnameVerifier / withCertificatePinner / withConnectionPool | 覆盖 TLS 与连接池配置 | 仅应用拦截器 |
retryOnConnectionFailure / followRedirects / followSslRedirects | 读取重试与重定向策略 | — |
eventListener: EventListener | 读取事件监听器 | — |
值得注意的是,这些withXxx覆盖能力让拦截器具备了“按请求调整客户端行为”的能力——例如按 URL 动态切换缓存策略或认证器——而不必为每种场景构建多个OkHttpClient。
二、拦截器链的完整组装顺序
文档提到“OkHttp 使用列表跟踪拦截器,拦截器按顺序调用”。从源码可以精确还原这条链的组装过程。在 RealCall.getResponseWithInterceptorChain() 中:
// Build a full stack of interceptors. val interceptors = mutableListOf<Interceptor>() interceptors += client.interceptors // ① 用户 Application 拦截器 interceptors += RetryAndFollowUpInterceptor() // ② 重试与重定向(内部) interceptors += BridgeInterceptor() // ③ 应用层与网络层协议桥接(内部) interceptors += CacheInterceptor() // ④ 缓存(内部) interceptors += ConnectInterceptor // ⑤ 建立连接(内部) if (!forWebSocket) { interceptors += client.networkInterceptors // ⑥ 用户 Network 拦截器 } interceptors += CallServerInterceptor // ⑦ 真正写出请求(内部) val chain = RealInterceptorChain( call = this, interceptors = interceptors, index = 0, exchange = null, request = originalRequest, )这个顺序解释了文档中应用/网络拦截器的行为差异:
- 用户 Application 拦截器位于链首,在重定向、重试、缓存判断之前执行,因此对一次
execute()只会被调用一次,且看到的是应用原始意图; RetryAndFollowUpInterceptor负责 3xx 重定向与连接失败重试,这正好解释了“应用拦截器只看到最终响应”;CacheInterceptor位于连接建立之前,当缓存直接命中时,请求根本不会走到网络侧,因此网络拦截器不会被调用;- 用户 Network 拦截器位于
ConnectInterceptor之后、CallServerInterceptor之前,此时连接已建立,所以chain.connection()非 null,且能看到即将在网络上发送的字节(含 OkHttp 注入的Accept-Encoding: gzip等头部); - WebSocket 调用不插入网络拦截器(
if (!forWebSocket)),这是网络拦截器不适用于 WebSocket 握手的源码依据。
注册入口在 OkHttpClient.Builder:addInterceptor()追加到interceptors列表,addNetworkInterceptor()追加到networkInterceptors列表。两者都提供了 inline lambda 重载,Kotlin 用户可以直接写client.addInterceptor { chain -> ... }。Builder的 KDoc 还对网络拦截器作出明确约定:“这些拦截器必须恰好调用proceed一次:网络拦截器短路或重复网络请求都是错误。”
三、Application 拦截器:一次调用看到最终结果
按文档示例,把上面的LoggingInterceptor注册为应用拦截器:
OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new LoggingInterceptor()) .build(); Request request = new Request.Builder() .url("http://www.publicobject.com/helloworld.txt") .header("User-Agent", "OkHttp Example") .build(); Response response = client.newCall(request).execute(); response.body().close();http://www.publicobject.com/helloworld.txt会 301 重定向到https://publicobject.com/helloworld.txt,而 OkHttp 会自动跟随重定向。此时应用拦截器只被调用一次,chain.proceed()返回的是重定向完成后的最终响应:
INFO: Sending request http://www.publicobject.com/helloworld.txt on null User-Agent: OkHttp Example INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms Server: nginx/1.4.6 (Ubuntu) Content-Type: text/plain Content-Length: 1759 Connection: keep-alive从日志可以读出三个关键信息:
on null——应用拦截器中chain.connection()返回 null,因为此时连接尚未建立(对应RealInterceptorChain构造函数接收的exchange = null);- 请求 URL 是
http://www.publicobject.com/...,响应 URL 已是https://publicobject.com/...——判断是否发生重定向的依据就是response.request().url()与request.url()不同; - 日志里没有
Host、Accept-Encoding等协议级头部,因为 BridgeInterceptor 还没执行。
四、Network 拦截器:每次网络往返都会被观察
注册网络拦截器只需把addInterceptor()换成addNetworkInterceptor():
OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor(new LoggingInterceptor()) .build(); // 其余 request / execute 代码同上由于重定向产生了两次真实网络请求(一次 HTTP、一次 HTTPS),网络拦截器会运行两次:
INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1} User-Agent: OkHttp Example Host: www.publicobject.com Connection: Keep-Alive Accept-Encoding: gzip INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms Server: nginx/1.4.6 (Ubuntu) Content-Type: text/html Content-Length: 193 Connection: keep-alive Location: https://publicobject.com/helloworld.txt INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1} User-Agent: OkHttp Example Host: publicobject.com Connection: Keep-Alive Accept-Encoding: gzip INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms Server: nginx/1.4.6 (Ubuntu) Content-Type: text/plain Content-Length: 1759 Connection: keep-alive与第一段日志对照,网络侧请求多出若干内容:
- 由 OkHttp 自动添加的
Accept-Encoding: gzip,用于宣告支持响应压缩; Host、Connection: Keep-Alive等协议级头部;- 非 null 的
Connection对象,可以看到hostAddress(解析到的 IP)、cipherSuite(TLS 密码套件)、protocol(http/1.1 或 h2)等实际连接细节。
网络拦截器的硬性约束(源码校验)
文档只是说网络拦截器“能观察中间响应”,而源码 RealInterceptorChain.proceed() 把约束变成了运行时的强制检查:
override fun proceed(request: Request): Response { check(index < interceptors.size) calls++ if (exchange != null) { check(exchange.finder.routePlanner.sameHostAndPort(request.url)) { "network interceptor ${interceptors[index - 1]} must retain the same host and port" } check(calls == 1) { "network interceptor ${interceptors[index - 1]} must call proceed() exactly once" } } // ... if (exchange != null) { check(index + 1 >= interceptors.size || next.calls == 1) { "network interceptor $interceptor must call proceed() exactly once" } } // ... }其中exchange != null正是“当前处于网络拦截器区段”的标志(RealInterceptorChain类注释说明:应用拦截器的链exchange必须为 null,网络拦截器的链则必须非 null)。由此得到三条硬规则:
- 网络拦截器必须恰好调用
proceed()一次,既不能短路(不 proceed)也不能重试(多次 proceed); - 网络拦截器不能改变请求的 host 和 port——它只能操作同一目标上的数据;
- 这些约束同样有回归测试保障,见 InterceptorTest.kt 中对 "must call proceed() exactly once" 与 "must retain the same host and port" 的断言。
同样的exchange == null检查也出现在所有“按调用覆盖配置”的方法上,例如 withConnectTimeout / withReadTimeout / withWriteTimeout 都会先check(exchange == null) { "Timeouts can't be adjusted in a network interceptor" },然后返回一个copy(...)出的新链。这解释了文档中“应用拦截器可用 withConnectTimeout、withReadTimeout、withWriteTimeout 调整 Call 超时”这一条——超时的动态调整只对应用拦截器开放。
五、Application 与 Network 拦截器如何选择
文档给出的取舍清单如下,这里结合源码机制一并整理:
| 维度 | Application 拦截器 | Network 拦截器 |
|---|---|---|
| 中间响应 | 无需关心重定向、重试等中间响应 | 能观察重定向、重试等中间响应 |
| 缓存 | 即使响应来自缓存也总是被调用 | 被缓存短路时不会被调用 |
| 观察到的数据 | 应用的原始意图,不含 OkHttp 注入的If-None-Match等头部 | 数据即网络上传输的原始形态(含Accept-Encoding: gzip等) |
| 连接信息 | chain.connection()恒为 null | 可访问承载请求的Connection(IP、TLS 配置) |
| 短路 | 允许短路,可以不调用proceed()(如直接返回缓存/假响应) | 禁止短路或重复proceed(),运行时强校验 |
| 重试 | 允许重试、多次调用proceed() | 禁止 |
| 超时/配置覆盖 | 可用withConnectTimeout/withReadTimeout/withWriteTimeout、withCache/withProxy/...按调用调整 | 不允许,check(exchange == null)会抛错 |
| WebSocket | 正常参与 | 不参与(forWebSocket时不插入网络拦截器) |
经验法则:需要“对每次业务调用恰好执行一次”的逻辑(鉴权头、全局超时策略、业务级假响应、指标统计)放 Application 层;需要“对每次真实网络字节负责”的逻辑(代理探测、网络级诊断、流量镜像)放 Network 层。
六、改写请求:以 Gzip 请求压缩为例
拦截器可以增删或替换请求头部,也可以转换携带正文的请求体。文档示例是一个请求体压缩拦截器:对已支持压缩的服务器,用Content-Encoding: gzip包装请求体:
/** This interceptor compresses the HTTP request body. Many webservers can't handle this! */ final class GzipRequestInterceptor implements Interceptor { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request originalRequest = chain.request(); if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) { return chain.proceed(originalRequest); } Request compressedRequest = originalRequest.newBuilder() .header("Content-Encoding", "gzip") .method(originalRequest.method(), gzip(originalRequest.body())) .build(); return chain.proceed(compressedRequest); } private RequestBody gzip(final RequestBody body) { return new RequestBody() { @Override public MediaType contentType() { return body.contentType(); } @Override public long contentLength() { return -1; // We don't know the compressed length in advance! } @Override public void writeTo(BufferedSink sink) throws IOException { BufferedSink gzipSink = Okio.buffer(new GzipSink(sink)); body.writeTo(gzipSink); gzipSink.close(); } }; } }这个实现展示了请求改写的两个要点:一是通过newBuilder()生成新 Request,而不是原地修改;二是包装RequestBody时contentLength()返回-1,因为压缩后的长度事先未知——OkHttp 会因此改用 chunked 传输。
仓库中还有一个对称方向的真实实现可以对照:CompressionInterceptor(响应压缩)。它在请求没有显式Accept-Encoding时注入Accept-Encoding头(算法列表拼成如br, gzip的形式),并在proceed()之后用decompress()包装响应体:
override fun intercept(chain: Interceptor.Chain): Response = if (algorithms.isNotEmpty() && chain.request().header("Accept-Encoding") == null) { val request = chain.request() .newBuilder() .header("Accept-Encoding", acceptEncoding) .build() val response = chain.proceed(request) decompress(response) } else { chain.proceed(chain.request()) }它同样演示了响应改写中的常见陷阱:解压后长度不再已知,所以 decompress() 会同时移除Content-Encoding和Content-Length头,再以-1长度重建 body——与文档 Gzip 请求例子的处理方式互为镜像。
七、改写响应:修正服务器错误的 Cache-Control
对称地,拦截器也可以改写响应头部、转换响应体。文档强调这比改写请求头部更危险,因为它可能违背服务器端的预期。一个典型场景是修正服务器配置错误的Cache-Control,以启用更好的缓存:
/** Dangerous interceptor that rewrites the server's cache-control header. */ private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Response originalResponse = chain.proceed(chain.request()); return originalResponse.newBuilder() .header("Cache-Control", "max-age=60") .build(); } };文档给出的最佳实践是:这种手法效果最好的场合,是配合服务器端的对应修复——即客户端拦截器作为过渡性补偿,而不是长期方案。
八、实现拦截器的实务要点
综合文档与源码,编写拦截器时值得注意以下几点:
proceed()至多对最终响应生效一次。应用拦截器若要重试,调用proceed()多次时,前一次返回的响应体必须先关闭(文档原文约束,也是避免连接泄漏的关键);- 拦截器必须返回非 null 的 Response。RealInterceptorChain.proceed() 对
interceptor.intercept(next)的结果做 null 检查并抛NullPointerException("interceptor $interceptor returned null"); - 失败时优先返回合成响应。接口 KDoc 给出的推荐模式是:校验不通过时构造一个带 4xx 状态码的
Response(Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1).code(400)...)直接返回,而不是抛出非 IO 异常触发调用取消; - Kotlin 用户可直接使用 lambda 注册:
Interceptor { chain -> ... }或builder.addInterceptor { chain -> ... }(见 Interceptor.kt L75-L81),适合行内、单点的拦截逻辑; - 按调用覆盖配置是应用拦截器的专属能力:
withCache()、withProxy()、withAuthenticator()、withTimeout系列在RealInterceptorChain中均以check(exchange == null)守卫,在网络拦截器中调用会立即抛出IllegalStateException; - 测试参照:InterceptorTest.kt 覆盖了短路、重复
proceed()、跨 host 改写等场景的正向与异常断言,可作为自研拦截器行为的对照基线。
小结
OkHttp 拦截器机制的核心可以概括为三层:
- 契约层:
Interceptor.intercept(chain)+chain.proceed(request),proceed是整条链上所有 HTTP 工作的触发点(Interceptor.kt); - 编排层:
RealCall按“应用拦截器 → 重试/重定向 → 桥接 → 缓存 → 连接 → 网络拦截器 → 服务器调用”的固定顺序组装RealInterceptorChain,应用与网络拦截器分别落在链的首部与近尾部,由此产生“一次调用 vs 每次网络往返”的行为分野(RealCall.kt L209-L230); - 约束层:网络拦截器必须恰好调用一次
proceed()、必须保留同一 host/port、不可覆盖超时与配置;应用拦截器则允许短路、重试与按调用覆盖配置(RealInterceptorChain.kt L311-L343)。
掌握这三层之后,无论是实现日志、鉴权、压缩,还是修正Cache-Control这类“危险但有效”的响应改写,都可以找到明确的落点与边界。
【免费下载链接】okhttpA meticulous HTTP client for the JVM, Android, and GraalVM.项目地址: https://gitcode.com/gh_mirrors/okh/okhttp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考