第3章:添加自定义拦截器的两种方式——addInterceptor()与addNetworkInterceptor()的区别与使用场景
3.1 拦截器体系的双层架构
OkHttp 的拦截器链采用双层设计:应用层拦截器(Application Interceptor)与网络层拦截器(Network Interceptor)。两者通过不同的注册入口添加到 OkHttpClient 中,并在请求/响应生命周期的不同阶段被调度。理解这两层架构是实现模块化网络层的前提。
应用层拦截器注册于 addInterceptor(),位于拦截器链的最外层,在 OkHttp 内部处理(重定向、重试、缓存)之前执行。网络层拦截器注册于 addNetworkInterceptor(),位于拦截器链的内层,在内部处理之后、实际网络 I/O 之前执行。
核心原则:应用层拦截器“一次请求只调用一次”;网络层拦截器“每次网络往返调用一次”。这是两者最本质的差异。
3.2 核心差异对比
| 对比维度 | addInterceptor() | addNetworkInterceptor() |
|---|---|---|
| 拦截层级 | 应用层(最外层) | 网络层(内层) |
| 调用时机 | 重定向/重试/缓存命中之前 | 重定向/重试/缓存穿透之后 |
| 调用次数 | 每个请求仅 1 次 | 每次网络往返触发 1 次(重定向时会多次) |
| 可访问的数据 | 原始请求 + 最终响应 | 每次网络层面的 request/response |
| 是否感知中间过程 | 不感知重定向/重试 | 能感知每次网络尝试 |
| 适用场景 | 日志记录、Header 注入、缓存策略、鉴权 | 重定向追踪、网络诊断、流量统计、Cookie 同步 |
3.3 双拦截器调用链路
下图展示了请求从应用层穿透到网络层,经过内部处理,最终返回响应的完整链路。两种拦截器位于不同的层级,决定了它们在请求生命周期中的行为差异。
3.4 代码实现与 API 使用
以下演示如何分别使用两种方式注册自定义拦截器,并观察它们在重定向场景下的行为差异。
3.4.1 自定义拦截器基础实现
public class LoggingInterceptor implements Interceptor {
private final String tag;
public LoggingInterceptor(String tag) {
this.tag = tag;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long t1 = System.nanoTime();
System.out.println(tag + " → 请求:"
+ request.method() + " " + request.url());
Response response = chain.proceed(request);
long t2 = System.nanoTime();
System.out.println(tag + " ← 响应:"
+ response.code() + " " + response.request().url()
+ " (耗时:" + (t2 - t1) / 1e6 + "ms)");
return response;
}
}
3.4.2 两种注册方式
OkHttpClient client = new OkHttpClient.Builder()
// 应用层拦截器 —— 每个请求只触发一次
.addInterceptor(new LoggingInterceptor("[App]"))
// 网络层拦截器 —— 每次网络往返触发一次
.addNetworkInterceptor(new LoggingInterceptor("[Net]"))
.build();
3.4.3 重定向场景行为验证
使用一个会触发 302 重定向的 URL 进行测试:
Request request = new Request.Builder()
.url("http://httpbin.org/redirect/2") // 会重定向 2 次
.build();
try (Response response = client.newCall(request).execute()) {
System.out.println("最终响应码:" + response.code());
}
输出结果(关键片段):
[App] → 请求:GET http://httpbin.org/redirect/2
[Net] → 请求:GET http://httpbin.org/redirect/2
[Net] ← 响应:302 ...
[Net] → 请求:GET http://httpbin.org/relative-redirect/1
[Net] ← 响应:302 ...
[Net] → 请求:GET http://httpbin.org/get
[Net] ← 响应:200 ...
[App] ← 响应:200 ...
可见 [App] 只输出一次(请求开始 + 最终响应),而 [Net] 输出了三次(每次重定向都触发)。
调试技巧:在开发阶段同时注册两种拦截器,通过日志对比调用次数,可以快速定位是哪个拦截器在处理链路中出现了预期外的行为。
3.5 典型使用场景分析
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 统一日志记录 | addInterceptor() |
只需记录一次请求与最终响应,避免重复日志 |
| 全局 Header 注入(如 ) | addInterceptor() |
只需注入一次,重定向后自动携带 |
| 缓存策略 / 离线模式 | addInterceptor() |
在缓存命中前拦截,避免无效网络调用 |
| 重定向链路追踪 | addNetworkInterceptor() |
能捕获每一次重定向的 request/response |
| 网络质量监控 / 流量统计 | addNetworkInterceptor() |
精确统计每次网络往返的延迟与字节数 |
| Cookie 自动同步 | addNetworkInterceptor() |
每次网络响应都能拿到最新的 Set-Cookie |
| 请求重试(自定义) | addNetworkInterceptor() |
能获取每次失败的详细信息,决定是否重试 |
3.6 调试方法与日志验证
- 启用 OkHttp 自带日志:引入
logging-interceptor依赖,分别用两种方式注册,观察输出差异。 - 自定义日志标签:在拦截器中注入不同 tag(如
[App]与[Net]),通过 grep 快速过滤。 - 触发重定向测试:使用
httpbin.org/redirect/N或本地测试接口,验证调用次数是否与预期一致。 - 缓存命中测试:配置缓存策略后,重复请求同一接口,观察应用层拦截器是否不触发。
# 命令行过滤日志示例(Android adb logcat)
adb logcat | grep -E "\[App\]|\[Net\]"
3.7 常见错误与避坑指南
错误1:在
解决:应用层拦截器尽量只做“只读”操作或添加非核心 Header;修改请求地址应放在网络层拦截器。
addInterceptor() 中修改请求的 Host 或 Scheme,导致 OkHttp 内部重定向逻辑失效。解决:应用层拦截器尽量只做“只读”操作或添加非核心 Header;修改请求地址应放在网络层拦截器。
错误2:在
解决:网络层拦截器必须设计为“可被多次调用”,避免副作用。
addNetworkInterceptor() 中假设请求只调用一次,编写了幂等性不安全的逻辑(如计数器累加)。解决:网络层拦截器必须设计为“可被多次调用”,避免副作用。
错误3:在拦截器中未调用
解决:每个拦截器必须且只能调用一次
chain.proceed(request) 或重复调用,导致请求中断或异常。解决:每个拦截器必须且只能调用一次
chain.proceed(),并返回其响应。
错误4:在应用层拦截器中处理
解决:Cookie 同步应放在网络层拦截器中,确保每次网络响应都被处理。
Set-Cookie,但重定向时中间响应的 Cookie 可能被遗漏。解决:Cookie 同步应放在网络层拦截器中,确保每次网络响应都被处理。
3.8 性能与稳定性优化
- 避免耗时操作:拦截器运行在 OkHttp 的调度线程中,任何阻塞(如数据库读写、网络请求)都会拖慢整个请求链。耗时操作应异步化或放到回调中处理。
- 控制拦截器数量:每增加一个拦截器,调用链就多一层包装。生产环境建议将同类逻辑合并到一个拦截器中,控制总数在 3~5 个以内。
- 资源释放:在拦截器中如果创建了临时资源(如 ByteString、Buffer),确保在返回 Response 前释放,避免内存泄漏。
- 网络层拦截器注意异常处理:网络层拦截器可能因重定向或重试被多次调用,内部的异常处理要保证幂等性和资源正确回收。
- 缓存策略搭配:如果只关心最终结果,优先使用
addInterceptor()实现缓存,避免网络层拦截器在缓存命中时被空跑。
选型总则:当你不确定该用哪种方式时,默认选择
addInterceptor()。只有当你需要“感知每次网络往返”或“处理重定向中间过程”时,才升级到 addNetworkInterceptor()。这条原则能覆盖 90% 以上的场景。