4. 添加网络拦截器(Network Interceptor)与缓存覆盖

网络拦截器是 OkHttp 拦截器链条中执行在 网络 I/O 之前、缓存策略判定之后 的特殊拦截器。它的执行时机决定了它可以修改即将发出的真实请求(Request)以及接收到的原始响应(Response),但对缓存是否命中、缓存响应是否返回 没有决定权——真正控制缓存行为的是 CacheInterceptor。本章深入剖析网络拦截器的执行时机、与 Application 拦截器的本质区别,以及如何在网络拦截器中覆盖请求缓存指令和响应缓存指令,实现精细化的缓存策略注入。

网络拦截器执行时机 & 缓存决策流程 Application<br/>拦截器链 Cache<br/>命中判定 命中 直接返回<br/>缓存响应 未命中 Network<br/>拦截器链 服务器 Network 拦截器<br/>处理响应 Cache 拦截器<br/>写入缓存 Application<br/>拦截器链 关键时序说明 • 网络拦截器仅在 Cache 判定未命中、真正发起网络请求后才执行。 • 响应返回时先经过 Network 拦截器 → Cache 拦截器(缓存写入)→ Application 拦截器。 • 网络拦截器可以修改请求的 Cache-Control 指令,影响缓存写入策略。 应用场景 在 Authentication 拦截器内添加 后,再在网络拦截器内强制覆盖 Cache-Control: no-cache 以避免敏感数据缓存。

4.1 网络拦截器执行时机

网络拦截器的关键特征在于它的触发条件:

  • 仅在需要真正网络请求时执行 —— 如果缓存命中且响应未过期,网络拦截器 不会 被调用。
  • 在缓存策略判定之后 —— OkHttp 内部先执行 CacheInterceptor 判断是否可以复用缓存,只有判定“需要走网络”时,才会调用网络拦截器列表。
  • 在写入缓存之前 —— 响应从服务器返回后,先经过网络拦截器,再传递给 CacheInterceptor 进行缓存写入。

这一执行顺序意味着网络拦截器 只能看到真正发出去的网络请求,无法感知缓存命中后直接返回的早停响应。

4.2 Application 拦截器 vs Network 拦截器

对比维度 Application Interceptor Network Interceptor
注册位置 addInterceptor() addNetworkInterceptor()
执行次数 每次调用都执行(包括缓存命中) 仅真正网络请求时执行
访问缓存 可以读取缓存响应,也可阻止缓存使用 通常不直接操作缓存(可间接通过指令覆盖)
重定向/重试 拦截器只看到最终请求/响应,不感知重定向 每次重定向/重试都会触发网络拦截器
典型用途 日志、认证、统一 header、请求/响应加密 重试策略、网络层缓存指令覆盖、流量监控

4.3 修改请求缓存指令

在网络拦截器中,可以调用 request.newBuilder() 修改请求的 Cache-Control 头,从而覆盖上层调用者指定的缓存策略。常见覆盖场景:

  • 强制不缓存:添加 Cache-Control: no-cache 或 no-store。
  • 强制刷新:设置 max-age=0 配合 no-cache。
  • 指定缓存有效期:修改 max-age 或 s-maxage。
public Response intercept(Chain chain) throws IOException {
    Request original = chain.request();
    // 强制跳过缓存(即使上层说要缓存)
    Request newRequest = original.newBuilder()
            .header("Cache-Control", "no-cache")
            .build();
    return chain.proceed(newRequest);
}

4.4 响应缓存指令覆盖

网络拦截器在拿到服务器返回的原始响应后,可以修改响应头部,影响 CacheInterceptor 写入缓存的策略。例如:

  • 将服务器返回的 Cache-Control: private 改为 public 以允许共享缓存。
  • 增加 max-age 延长缓存时间。
  • 移除 Set-Cookie 避免敏感信息被缓存。
public Response intercept(Chain chain) throws IOException {
    Response originalResponse = chain.proceed(chain.request());
    // 覆盖缓存指令:延长缓存到10分钟
    return originalResponse.newBuilder()
            .header("Cache-Control", "public, max-age=600")
            .removeHeader("Pragma")
            .removeHeader("Set-Cookie")
            .build();
}
⚠️ 注意:修改响应头部可能违反 HTTP 语义,建议仅在明确业务场景下使用,并配合日志记录。

4.5 实现流程与代码示例

标准实现步骤:

  1. 创建实现 Interceptor 接口的类。
  2. 在 intercept() 方法内,先通过 chain.request() 获取原始请求。
  3. 按需修改请求头(覆盖缓存指令)。
  4. 调用 chain.proceed(modifiedRequest) 发起网络请求。
  5. 获取响应后,按需修改响应头(覆盖缓存指令)。
  6. 返回修改后的响应。
  7. 使用 OkHttpClient.Builder.addNetworkInterceptor() 注册。
public class CacheOverwriteInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        // 1. 修改请求缓存指令:强制使用网络
        Request request = chain.request().newBuilder()
                .header("Cache-Control", "no-cache")
                .build();

        // 2. 执行网络请求
        Response response = chain.proceed(request);

        // 3. 修改响应缓存指令:统一设置 5 分钟缓存
        return response.newBuilder()
                .header("Cache-Control", "public, max-age=300")
                .build();
    }
}

// 在 OkHttpClient 中注册
OkHttpClient client = new OkHttpClient.Builder()
        .addNetworkInterceptor(new CacheOverwriteInterceptor())
        .cache(new Cache(cacheDir, 10 * 1024 * 1024))
        .build();

4.6 调试与验证

推荐使用 日志拦截器 配合网络拦截器观察缓存行为:

  • 在 Application 拦截器日志中查看是否走了网络(观察请求/响应头中是否有 Cache-Control 变化)。
  • 在 Network 拦截器日志中确认每次重定向是否触发。
  • 使用 curl -I URL 对比服务器原响应与客户端实际缓存策略。
// 日志输出示例(HttpLoggingInterceptor)
D/OkHttp: --> GET https://api.example.com/data
D/OkHttp: --> Cache-Control: no-cache
D/OkHttp: <-- 200 OK (120ms)
D/OkHttp: Cache-Control: public, max-age=300

通过对比请求/响应头中的 Cache-Control 是否与预期一致,验证覆盖是否生效。

4.7 常见错误与避坑

错误场景 原因 解决方案
网络拦截器未生效 缓存命中,请求并未到达网络层 检查缓存命中日志;强制设置 no-cache 在 Application 拦截器层
修改请求指令被后续拦截器覆盖 网络拦截器顺序不对 确保 CacheOverwriteInterceptor 是最后一个网络拦截器
响应修改后缓存仍写入原始指令 CacheInterceptor 在 Network 拦截器之后执行,但 Cache 可能使用未修改的响应 确认修改后的 Response 对象传递给 chain.proceed() 的返回值
过度覆盖导致服务器禁止缓存 对敏感数据强制 no-cache 反而增加服务器负载 只在明确需要覆盖的场景使用,配合业务标识判断

4.8 性能与稳定性优化

  • 避免在网络拦截器中执行耗时操作(如 I/O、加密),否则会阻塞整个连接池。
  • 不要修改请求 body(除非知道自己在干什么),容易导致流无法重复读。
  • 对响应修改后必须关闭原始 body(使用 response.body().close() 或重建 Response 时自动处理),防止资源泄露。
  • 使用 @Nullable 注解处理空缓存,避免在无缓存目录时抛出 NPE。
  • 减少不必要的缓存指令覆盖:每覆盖一次指令,OkHttp 内部需重新解析头信息,高频覆盖会降低吞吐。
💡 最佳实践:将网络拦截器中的缓存覆盖逻辑封装成独立策略类,通过配置中心动态下发,实现线上无重启调整缓存策略。