4. 添加网络拦截器(Network Interceptor)与缓存覆盖
网络拦截器是 OkHttp 拦截器链条中执行在 网络 I/O 之前、缓存策略判定之后 的特殊拦截器。它的执行时机决定了它可以修改即将发出的真实请求(Request)以及接收到的原始响应(Response),但对缓存是否命中、缓存响应是否返回 没有决定权——真正控制缓存行为的是 CacheInterceptor。本章深入剖析网络拦截器的执行时机、与 Application 拦截器的本质区别,以及如何在网络拦截器中覆盖请求缓存指令和响应缓存指令,实现精细化的缓存策略注入。
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 实现流程与代码示例
标准实现步骤:
- 创建实现
Interceptor接口的类。 - 在
intercept()方法内,先通过chain.request()获取原始请求。 - 按需修改请求头(覆盖缓存指令)。
- 调用
chain.proceed(modifiedRequest)发起网络请求。 - 获取响应后,按需修改响应头(覆盖缓存指令)。
- 返回修改后的响应。
- 使用
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 内部需重新解析头信息,高频覆盖会降低吞吐。
💡 最佳实践:将网络拦截器中的缓存覆盖逻辑封装成独立策略类,通过配置中心动态下发,实现线上无重启调整缓存策略。