4. 自定义缓存拦截器:基本骨架与核心协议
自定义缓存拦截器是实现精细化缓存控制的关键。通过实现 Interceptor 接口,开发者可以完全掌控请求与响应的代理逻辑,包括缓存策略注入、响应缓存写入以及离线兜底机制。
4.1 Interceptor 接口契约
OkHttp 的 Interceptor 接口定义了网络拦截与应用程序拦截的统一协议。缓存拦截器通常注册为 应用拦截器,确保在重定向与重试之前执行。
| 方法 | 参数 | 返回值 | 职责 |
|---|---|---|---|
intercept(Chain chain) |
Interceptor.Chain |
Response |
拦截原始请求,注入缓存策略;代理至下一个拦截器;处理响应并写入缓存 |
核心方法只有一个 intercept,其中 Chain 对象提供了访问原始 Request、proceed(Request) 向下传递请求、以及获取连接信息的能力。
4.2 拦截器基本骨架
一个完整的缓存拦截器需要分为三个阶段处理:
- 请求阶段:检测网络状态,为 Request 添加 Cache-Control 头或失败降级策略。
- 代理阶段:调用
chain.proceed(request)执行实际网络请求。 - 响应阶段:从响应中提取数据,写入本地缓存,并封装 CacheStrategy 结果。
骨架代码实现
public class CacheInterceptor implements Interceptor {
private final Context context;
private final CacheStrategy strategy;
public CacheInterceptor(Context context, CacheStrategy strategy) {
this.context = context;
this.strategy = strategy;
}
@Override
public Response intercept(Chain chain) throws IOException {
// 1. 获取原始请求
Request originalRequest = chain.request();
// 2. 根据网络状态与策略构建缓存请求
Request cacheRequest = buildCacheRequest(originalRequest);
// 3. 代理请求 —— 执行网络或缓存响应
Response response;
try {
response = chain.proceed(cacheRequest);
} catch (IOException e) {
// 4. 网络失败时尝试返回缓存
Response cachedResponse = retrieveCacheResponse(originalRequest);
if (cachedResponse != null) {
return cachedResponse;
}
throw e;
}
// 5. 写入缓存并封装响应
Response finalResponse = cacheAndWrapResponse(originalRequest, response);
return finalResponse;
}
private Request buildCacheRequest(Request original) {
// 实际实现:注入 Cache-Control,添加离线标记等
if (!NetworkUtil.isConnected(context)) {
return original.newBuilder()
.header("Cache-Control", "only-if-cached")
.header("Prefer", "stale")
.build();
}
return original.newBuilder()
.header("Cache-Control", strategy.getRequestDirective())
.build();
}
private Response retrieveCacheResponse(Request request) {
// 从本地磁盘缓存中查找匹配的响应
return strategy.getFromCache(request);
}
private Response cacheAndWrapResponse(Request request, Response serverResponse) {
// 将服务器响应写入缓存,并附加缓存元数据
CacheEntry entry = strategy.putToCache(request, serverResponse);
return serverResponse.newBuilder()
.header("X-Cache-Status", entry.isFresh() ? "HIT" : "MISS")
.header("X-Cache-Age", String.valueOf(entry.getAge()))
.build();
}
}
4.3 内联 SVG:拦截器代理流程
下图展示了自定义缓存拦截器在请求链中的核心代理流程:
4.4 CacheStrategy 设计要点
拦截器依赖 CacheStrategy 对象控制缓存行为,该策略接口通常包含以下关键方法:
| 方法签名 | 输入 | 输出 | 说明 |
|---|---|---|---|
getRequestDirective() |
无 | String |
返回 Cache-Control 指令,如 max-age=60 |
getFromCache(Request) |
原始请求 | Response / null |
从磁盘缓存查找匹配响应 |
putToCache(Request, Response) |
请求与服务器响应 | CacheEntry |
将响应写入缓存并返回元数据 |
isCacheValid(CacheEntry) |
缓存条目 | boolean |
判定缓存是否新鲜可用 |
实践建议:在 buildCacheRequest() 中结合 NetworkUtil.isConnected() 判断网络可用性。无网时强制添加 only-if-cached 头,避免触发网络请求导致崩溃。
4.5 常见错误与避坑
- 请求头冲突:如果在 Retrofit 注解中已经添加了
@Headers,拦截器追加同名头会导致多个值。建议拦截器内先清除原头再覆盖。 - 缓存写入失败:响应体只能被消费一次,写入缓存时需要 clone 响应体或使用
PeekSource,否则网络响应无法返回给客户端。 - 线程安全:
putToCache操作涉及磁盘 IO,务必使用同步块或ReentrantLock保护缓存文件,避免多请求并发写入脏数据。
// 错误示范:直接消费响应体导致空响应
Response response = chain.proceed(request);
String body = response.body().string(); // 此处已读取并关闭
// 后续将 body 写入缓存,但原始 response 无法再传递给客户端
// 正确做法:克隆响应体
Response clone = response.newBuilder()
.body(ResponseBody.create(response.body().contentType(), cachedBytes))
.build();
return clone;
4.6 调试与验证
在拦截器中添加日志输出,确认请求头与缓存状态:
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Log.d("CacheInterceptor", "Request URL: " + request.url());
Log.d("CacheInterceptor", "Cache-Control: " + request.header("Cache-Control"));
Response response = chain.proceed(request);
Log.d("CacheInterceptor", "X-Cache-Status: " + response.header("X-Cache-Status"));
return response;
}
部署提醒:自定义缓存拦截器中不要依赖 Application 级别的全局变量,推荐通过构造器注入 Context 与 CacheStrategy,以便单元测试时替换 Mock 对象。