OkHttp之所以能从众多网络库中脱颖而出,拦截器机制功不可没。它的设计思想是把一次网络请求拆分成多个可插拔的环节,每个拦截器只负责一件事:有的负责重试,有的负责桥接,有的负责缓存,有的负责真正建立连接发送数据。这些拦截器按照固定顺序组成一条链,请求从链头走到链尾,响应再从链尾走回链头,任何一层都可以中途拦截或者修改数据。理解了这个模型,你会发现OkHttp的很多高级功能——比如全局日志、统一鉴权、动态切换Host——本质上都是往这条链里插入自定义拦截器而已。

接下来我们看看OkHttp内部默认的五个拦截器是如何分工的。源码中,RealCall的getResponseWithInterceptorChain方法按顺序添加了:RetryAndFollowUpInterceptor(重试与重定向)、BridgeInterceptor(桥接,处理请求头和响应体)、CacheInterceptor(缓存)、ConnectInterceptor(建立连接)、CallServerInterceptor(发送请求读取响应)。这个顺序不能随意打乱,因为后面的拦截器依赖前面准备的数据。比如CacheInterceptor只有在请求头和URL被BridgeInterceptor规范化之后,才能正确生成缓存键。自定义拦截器会被添加到这个默认链的前面,因此它在请求真正发出之前和响应返回客户端之前都能插手。
从零开始写一个自定义拦截器
实现一个自定义拦截器只需要实现Interceptor接口,重写intercept方法。方法的参数Chain对象提供了request()获取当前请求,proceed(Request)将请求交给下一个拦截器并同步返回响应。下面是一个最简单的日志拦截器,它记录每个请求的地址和耗时,而且不会修改请求或响应。
class LoggingInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long start = System.nanoTime();
Response response = chain.proceed(request);
long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
Log.d("OkHttp", "请求地址: " + request.url() + " 耗时: " + duration + "ms");
return response;
}
}
把这个拦截器添加到OkHttpClient中很简单,调用addInterceptor方法即可。需要注意的是,这里通过Builder模式添加拦截器后,原有的client配置不变,生成的新client会带上这个拦截器。实际开发中,日志不应该包含请求体和响应体里的敏感信息,因此通常只记录url、状态码和耗时,避免把用户的密码或token打到logcat里。
另一个高频场景是Token过期自动刷新。很多后端接口会在token失效时返回HTTP 401,客户端需要重新获取token并重试原请求。拦截器正好适合做这件事,因为它可以在响应返回时检查状态码,然后重新构造请求再次走一遍链。下面是一个简化的实现,真实项目中需要处理并发刷新和重试上限的问题。
class TokenRefreshInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Response response = chain.proceed(original);
if (response.code() == 401) {
// 同步刷新Token,假设refreshToken()会阻塞直到拿到新Token
String newToken = refreshToken();
Request newRequest = original.newBuilder()
.header("Authorization", "Bearer " + newToken)
.build();
response.close(); // 关闭旧响应,释放资源
return chain.proceed(newRequest);
}
return response;
}
}
上面的代码有一个明显缺陷:如果刷新token之后请求还是返回401,就会陷入无限循环。解决方案是给请求设置一个重试标记,比如在header里加一个自定义字段表示已经重试过,或者用AtomicInteger计数。同时,多个请求同时遇到401时,应该只让一个线程去刷新token,其他线程等待结果,否则可能造成token反复刷新。这些细节在实际项目中比拦截器本身更容易出问题。
拦截器链中的线程与异常处理
拦截器的intercept方法运行在哪个线程?答案是:同步请求时运行在调用者的线程,异步请求时运行在OkHttp内部的线程池中。正因如此,拦截器内部不能直接更新UI,也不能执行特别耗时的操作,否则会阻塞整个请求链,导致同一条链上的其他请求排队。比如你在拦截器里做了一次大文件的MD5计算或者调用了一个慢速的远程服务,那么所有经过这个client的请求都会变慢。如果确实需要在拦截器中做耗时处理,应该使用同步锁配合子线程等待,或者把逻辑移到更外层的协程中,但拦截器本身必须同步返回Response。
异常处理是另一个容易忽略的坑。如果拦截器proceed过程中抛出IOException,这个异常会直接向上传递给调用方。如果是异步请求,会通过Callback的onFailure回调抛出。因此,如果你在拦截器里捕获了异常却又没重新抛出,请求很可能会被错误地吞掉,导致上层以为请求成功但实际没有拿到数据。除非你有明确的兜底策略,否则不要随意吞掉异常。另外,OkHttp的Call对象只能执行一次,不能在拦截器之外再次执行同一个Call。重试逻辑必须像Token刷新那样在拦截器内部重新构建Request并再次调用chain.proceed,而不是重新执行整个Call。
application拦截器与network拦截器的区别
OkHttp提供了两个添加拦截器的入口:addInterceptor和addNetworkInterceptor。前者添加的application拦截器在请求链的最外层,只会执行一次;后者添加的network拦截器在ConnectInterceptor之后、CallServerInterceptor之前,可能因为重试和重定向执行多次。这个区别决定了什么逻辑放在哪里。比如全局日志、统一鉴权、加解密这类只需要做一次的操作,放到application拦截器即可。而监控网络层实际传输的数据、修改请求头里与连接相关的字段(比如Host、Connection),就必须用network拦截器,因为application拦截器看到的请求可能还没经过BridgeInterceptor的规范化。
举一个具体例子:如果你想给所有请求添加一个自定义的header,比如设备标识,用application拦截器就够了。但如果你想查看重定向之后最终请求的URL,或者统计真实的网络请求次数,就需要network拦截器,因为application拦截器不会感知到内部重试。再比如缓存策略,CacheInterceptor本身也是个内置拦截器,你如果自己写了缓存逻辑,最好不要和它冲突。通常建议按照官方文档:application拦截器用于应用级逻辑,network拦截器用于网络传输层逻辑。两个入口混用最常见的问题是:把修改请求头的逻辑放在application拦截器里,结果发现有些请求头被后面的BridgeInterceptor覆盖了,排查半天才发现层次不对。
深入内置拦截器:重试、缓存与连接复用
RetryAndFollowUpInterceptor是链上的第一个拦截器,它负责处理连接失败的重试、HTTP重定向以及认证挑战。它的重试并不是无脑的,只有在请求体可以重复发送(比如请求体为空或者已经缓冲)并且连接被复用的情况下才会重试。如果请求体是一个大文件流且没有被缓存,发生网络错误时无法重新发送,这个拦截器就会放弃重试。这解释了为什么有时网络抖动一次请求就会失败,而有时能自动恢复。
CacheInterceptor负责实现HTTP缓存语义。它先根据请求生成缓存Key,然后查询本地缓存。如果缓存新鲜就直接返回缓存的Response;如果过期则继续走后面的拦截器获取新响应,然后根据响应头决定是否更新缓存。这个拦截器的行为完全受HTTP缓存头控制,比如Cache-Control、Expires、ETag等。如果你在服务端没有正确设置缓存头,OkHttp的缓存几乎不会生效。很多开发者抱怨“我用了OkHttp的缓存为什么没用”,往往是因为响应头里缺少缓存指令。另外,networkSecurityConfig对HTTPS证书的配置不影响缓存逻辑,但会直接影响连接建立,这属于ConnectInterceptor的范畴。
ConnectInterceptor负责从连接池获取一个可用的连接,或者建立新的TCP连接和TLS握手。连接复用是OkHttp性能优秀的关键:同一个主机和端口的请求可以共享同一个连接,避免频繁握手。连接池的大小、空闲时间等参数可以通过ConnectionPool配置。CallServerInterceptor则是链上的最后一环,它把请求头和请求体写入Socket,然后读取响应头和响应体。这个拦截器一旦执行,就意味着前面所有拦截器都同意放行这个请求。如果你在network拦截器里修改了请求体,但Content-Length没同步更新,CallServerInterceptor会抛出ProtocolException,这类错误排查起来比较费时,所以修改请求体时务必重新计算长度头。
OkHttp拦截器Android网络请求网络引擎修改时间:2026-09-20 08:53:29