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:拦截器代理流程

下图展示了自定义缓存拦截器在请求链中的核心代理流程:

自定义缓存拦截器代理流程 自定义缓存拦截器代理流程 客户端请求 请求阶段:注入缓存策略 网络请求(proceed) 响应阶段:缓存判定 写入缓存 + 封装响应 返回给客户端 网络异常:获取缓存 返回缓存响应 关键点 • 请求阶段:注入 Cache-Control • 代理阶段:chain.proceed() • 响应阶段:缓存写入 + 封装

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 对象。