第3章:添加自定义应用拦截器

应用拦截器(Application Interceptor)是OkHttp拦截器体系中最靠近调用方的一层。本章将围绕缓存头部注入、条件请求处理、响应缓存策略判断以及拦截器链调用四个核心环节,构建一个可复用的自定义应用拦截器。你将在理解拦截器调用模型的基础上,完成从代码编写到调试验证的完整路径。

学习目标
· 理解 Application Interceptor 与 Network Interceptor 的差异
· 掌握 Request.Builder 添加缓存头部(Cache-Control、If-None-Match 等)的方法
· 实现条件请求(Conditional GET)的逻辑分支
· 编写响应缓存有效性判断(如 max-age、must-revalidate)
· 正确调用 chain.proceed() 并处理缓存写入

3.1 原理概述

OkHttp 的拦截器链分为两种:Application Interceptor(应用拦截器)在请求发送前和响应返回后各触发一次,始终位于链的首尾;Network Interceptor(网络拦截器)位于连接和缓存中间件之后。对于缓存控制而言,应用拦截器最适合用于附加客户端全局缓存策略、注入条件请求头,并在响应到达后判断是否使用缓存副本。

下图展示了应用拦截器在 OkHttp 调用链中的位置以及本章涉及的三个关键处理阶段:

应用拦截器缓存处理流程 客户端请求 应用拦截器 intercept() A. 注入缓存头部 Cache-Control / If-None-Match B. 条件请求判断 If-Modified-Since / ETag chain.proceed() 响应缓存策略判断 应用拦截器在 chain.proceed() 前后均可以操作请求与响应,实现缓存逻辑

3.2 实现流程

自定义应用拦截器的完整实现分为以下六个步骤:

  1. 创建拦截器类:实现 Interceptor 接口,重写 intercept() 方法。
  2. 解析原始请求:从 Chain 对象获取 Request 实例。
  3. 注入缓存头部:通过 Request.Builder 添加 Cache-Control 和条件请求头。
  4. 条件请求处理:根据本地缓存元数据(如 ETag、Last-Modified)决定是否附加 If-None-Match 或 If-Modified-Since。
  5. 调用拦截器链:执行 chain.proceed(request) 获取响应。
  6. 响应缓存策略判断:基于响应状态码和首部(Cache-Control、Expires、Age)决定是否缓存当前响应。

3.3 关键代码实现

以下是完整的自定义应用拦截器代码,包含注释说明:

public class CacheApplicationInterceptor implements Interceptor {
    private static final String TAG = "CacheInterceptor";
    // 本地缓存元数据存储(生产环境应使用持久化存储)
    private final Map<String, CacheEntry> localCache = new ConcurrentHashMap<>();

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request originalRequest = chain.request();

        // 1. 注入 Cache-Control 头部(禁止中间代理缓存)
        Request.Builder builder = originalRequest.newBuilder()
                .header("Cache-Control", "max-age=0, no-cache, must-revalidate");

        // 2. 条件请求处理:检查本地缓存中是否有 ETag/Last-Modified
        String cacheKey = originalRequest.url().toString();
        CacheEntry entry = localCache.get(cacheKey);
        if (entry != null) {
            if (entry.etag != null) {
                builder.header("If-None-Match", entry.etag);
            }
            if (entry.lastModified != null) {
                builder.header("If-Modified-Since", entry.lastModified);
            }
        }

        Request modifiedRequest = builder.build();

        // 3. 调用拦截器链
        Response response = chain.proceed(modifiedRequest);

        // 4. 响应缓存策略判断
        if (response.code() == 304) {
            // 服务器返回 304 Not Modified,使用本地缓存响应
            return response.newBuilder()
                    .body(entry.cachedResponseBody != null ?
                          ResponseBody.create(entry.contentType, entry.cachedResponseBody) :
                          ResponseBody.create(null, ""))
                    .build();
        }

        // 5. 根据 Cache-Control 决定是否缓存
        String cacheControl = response.header("Cache-Control");
        if (cacheControl != null && !cacheControl.contains("no-store")) {
            // 读取并保存响应体到字节数组(只能消费一次)
            BufferedSource source = response.body().source();
            source.request(Long.MAX_VALUE);
            Buffer buffer = source.getBuffer();
            byte[] bodyBytes = buffer.readByteArray();

            // 提取缓存元数据
            CacheEntry newEntry = new CacheEntry();
            newEntry.etag = response.header("ETag");
            newEntry.lastModified = response.header("Last-Modified");
            newEntry.contentType = response.body().contentType();
            newEntry.cachedResponseBody = bodyBytes;
            newEntry.expirationTime = parseMaxAge(cacheControl);
            localCache.put(cacheKey, newEntry);

            // 重新构造带有缓存的响应(因为 body 已被消费)
            response = response.newBuilder()
                    .body(ResponseBody.create(response.body().contentType(), bodyBytes))
                    .build();
        }

        return response;
    }

    // 缓存元数据辅助类
    private static class CacheEntry {
        String etag;
        String lastModified;
        MediaType contentType;
        byte[] cachedResponseBody;
        long expirationTime; // 纳秒
    }

    // 简单解析 max-age 秒数(完整实现需处理所有 Cache-Control 指令)
    private long parseMaxAge(String cacheControl) {
        for (String part : cacheControl.split(",")) {
            part = part.trim();
            if (part.startsWith("max-age=")) {
                try {
                    return System.nanoTime() + Long.parseLong(part.substring(8)) * 1_000_000_000L;
                } catch (NumberFormatException e) {
                    return 0;
                }
            }
        }
        return 0;
    }
}

3.4 注册与调试

将自定义拦截器注册到 OkHttpClient 时,必须使用 addInterceptor()(应用层),而非 addNetworkInterceptor():

OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new CacheApplicationInterceptor())
        .build();

调试技巧:

  • 使用 OkHttp 的 EventListener 或日志拦截器打印请求和响应头,确认 If-None-Match、Cache-Control 是否已注入。
  • 利用抓包工具(如 Charles、Wireshark)观察实际发出的 HTTP 头部。
  • 在拦截器中添加断点,检查 chain.proceed() 返回的响应状态码是否为 304 或 200。
调试示例日志:
D/CacheInterceptor: Request headers: Cache-Control: max-age=0, no-cache, must-revalidate
D/CacheInterceptor: If-None-Match: "abc123"
D/CacheInterceptor: Response status: 304 Not Modified
D/CacheInterceptor: Using cached body for URL: https://api.example.com/data

3.5 常见错误

错误场景原因解决方案
响应体被消费后返回空在 intercept() 中两次读取 response.body() 的流使用 Buffer 保存字节数组后重构 Response.body()
304 响应无有效缓存体条件请求命中但本地缓存已被清除或尚未存储检查 CacheEntry.cachedResponseBody 非空
Cache-Control 头部冲突应用拦截器与网络拦截器同时添加相同头部统一在应用拦截器中管理缓存头部
内存溢出大文件响应体未限制缓存大小增加大小判断,只缓存 <10MB 的响应

3.6 性能与稳定性优化

  • 限制缓存大小:使用 LRU 算法淘汰旧缓存,避免 Map 无限增长。
  • 异步写入:将缓存条目写入磁盘或内存的操作移到异步线程,不阻塞拦截器链。
  • 缓存过期检查:在注入条件请求头前先判断本地缓存是否仍有效(基于 max-age),若有效则直接返回缓存响应,跳过网络请求。
  • 避免重复拷贝:使用 Buffer 的 clone() 或 ByteString 减少字节复制。
  • 处理 304 响应体:某些服务器在 304 时仍可能带有很小的消息体,必须忽略 body 并复用本地缓存。
注意: 自定义应用拦截器内进行缓存操作时,切勿调用 response.close() 或 response.body().close(),否则会导致连接泄漏。OkHttp 会自动处理响应体的关闭。

通过以上实现,你已经掌握了应用拦截器处理缓存的核心模式:在 intercept() 方法中利用 Request.Builder 修改请求头部,处理条件请求,并在响应返回后根据缓存策略决定是否持久化。下一章将深入网络拦截器的缓存协作机制。