第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 双拦截器调用链路

下图展示了请求从应用层穿透到网络层,经过内部处理,最终返回响应的完整链路。两种拦截器位于不同的层级,决定了它们在请求生命周期中的行为差异。

OkHttp 双拦截器调用链路 应用层 网络层 请求开始 Application Interceptor addInterceptor() OkHttp 核心处理 重定向 · 重试 · 缓存命中 Network Interceptor addNetworkInterceptor() Socket 网络 I/O 响应逐层返回 关键行为差异 ① 调用次数不同 应用层:1 次/请求 网络层:N 次/请求 ② 重定向可见性 应用层:不可见 网络层:每次重定向可见 ③ 缓存影响 应用层:缓存命中时不触发 网络层:缓存命中不触发 ④ 适用场景 应用层:日志/鉴权/Header 网络层:诊断/重定向追踪

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 调试方法与日志验证

  1. 启用 OkHttp 自带日志:引入 logging-interceptor 依赖,分别用两种方式注册,观察输出差异。
  2. 自定义日志标签:在拦截器中注入不同 tag(如 [App] 与 [Net]),通过 grep 快速过滤。
  3. 触发重定向测试:使用 httpbin.org/redirect/N 或本地测试接口,验证调用次数是否与预期一致。
  4. 缓存命中测试:配置缓存策略后,重复请求同一接口,观察应用层拦截器是否不触发。
# 命令行过滤日志示例(Android adb logcat)
adb logcat | grep -E "\[App\]|\[Net\]"

3.7 常见错误与避坑指南

错误1:在 addInterceptor() 中修改请求的 Host 或 Scheme,导致 OkHttp 内部重定向逻辑失效。
解决:应用层拦截器尽量只做“只读”操作或添加非核心 Header;修改请求地址应放在网络层拦截器。
错误2:在 addNetworkInterceptor() 中假设请求只调用一次,编写了幂等性不安全的逻辑(如计数器累加)。
解决:网络层拦截器必须设计为“可被多次调用”,避免副作用。
错误3:在拦截器中未调用 chain.proceed(request) 或重复调用,导致请求中断或异常。
解决:每个拦截器必须且只能调用一次 chain.proceed(),并返回其响应。
错误4:在应用层拦截器中处理 Set-Cookie,但重定向时中间响应的 Cookie 可能被遗漏。
解决:Cookie 同步应放在网络层拦截器中,确保每次网络响应都被处理。

3.8 性能与稳定性优化

  • 避免耗时操作:拦截器运行在 OkHttp 的调度线程中,任何阻塞(如数据库读写、网络请求)都会拖慢整个请求链。耗时操作应异步化或放到回调中处理。
  • 控制拦截器数量:每增加一个拦截器,调用链就多一层包装。生产环境建议将同类逻辑合并到一个拦截器中,控制总数在 3~5 个以内。
  • 资源释放:在拦截器中如果创建了临时资源(如 ByteString、Buffer),确保在返回 Response 前释放,避免内存泄漏。
  • 网络层拦截器注意异常处理:网络层拦截器可能因重定向或重试被多次调用,内部的异常处理要保证幂等性和资源正确回收。
  • 缓存策略搭配:如果只关心最终结果,优先使用 addInterceptor() 实现缓存,避免网络层拦截器在缓存命中时被空跑。
选型总则:当你不确定该用哪种方式时,默认选择 addInterceptor()。只有当你需要“感知每次网络往返”或“处理重定向中间过程”时,才升级到 addNetworkInterceptor()。这条原则能覆盖 90% 以上的场景。