恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
OkHttp 拦截器深入解析:Application 与 Network 拦截器的机制、链路与源码实现
首页
资讯中心
/
OkHttp 拦截器深入解析:Application 与 Network 拦截器的机制、链路与源码实现
OkHttp 拦截器深入解析:Application 与 Network 拦截器的机制、链路与源码实现
发布时间:2026/9/18 21:52:26
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应用拦截器中恒为 nullcall(): 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 mutableListOfInterceptor() 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.BuilderaddInterceptor()追加到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, proxyDIRECT hostAddress54.187.32.157 cipherSuitenone protocolhttp/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, proxyDIRECT hostAddress54.187.32.157 cipherSuiteTLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocolhttp/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、cipherSuiteTLS 密码套件、protocolhttp/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 cant 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可访问承载请求的ConnectionIP、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 cant 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 dont 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 servers 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-age60) .build(); } };文档给出的最佳实践是这种手法效果最好的场合是配合服务器端的对应修复——即客户端拦截器作为过渡性补偿而不是长期方案。八、实现拦截器的实务要点综合文档与源码编写拦截器时值得注意以下几点proceed()至多对最终响应生效一次。应用拦截器若要重试调用proceed()多次时前一次返回的响应体必须先关闭文档原文约束也是避免连接泄漏的关键拦截器必须返回非 null 的 Response。RealInterceptorChain.proceed() 对interceptor.intercept(next)的结果做 null 检查并抛NullPointerException(interceptor $interceptor returned null)失败时优先返回合成响应。接口 KDoc 给出的推荐模式是校验不通过时构造一个带 4xx 状态码的ResponseResponse.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),仅供参考