4. 缓存键生成规则与请求方法缓存行为:GET/POST/PUT 的缓存差异

OkHttp 的磁盘缓存机制依赖一个唯一键(Cache Key)来标识每个请求的响应。缓存键的生成规则直接决定了 缓存命中率 和 一致性。同时,HTTP 协议对不同请求方法(GET / POST / PUT)的缓存语义有明确限制,OkHttp 严格遵循这些语义。理解缓存键的构成以及方法差异,是正确配置离线缓存的前提。

4.1 缓存键生成原理

OkHttp 使用 CacheStrategy.Companion.urlToKey(Request) 方法生成缓存键。核心规则如下:

  • 基础成分:请求的 URL(包含协议、主机、端口、路径和查询参数)。
  • 方法影响:仅 GET 响应会被写入缓存;POST 和 PUT 的响应不会被 OkHttp 缓存,因此不会参与缓存键查找。
  • 请求头排除:Authorization、Cookie、User-Agent 等头部不被纳入缓存键(防止因动态头导致缓存碎片)。
  • 缓存键格式:采用 MD5 对标准化后的字符串进行哈希,生成 32 位十六进制文件名。
关键提示: 缓存键不考虑请求体(body),因此相同 URL 的 POST 和 GET 请求在缓存键层面可能冲突,但 OkHttp 通过方法级过滤避免了误用。

4.2 GET / POST / PUT 的缓存行为差异

请求方法 默认缓存响应 写入磁盘 命中条件 典型场景
GET 是 是(满足 Cache-Control 时) URL + 请求头匹配 读取新闻列表、用户信息
POST 否(除 303 重定向) 否 永不命中(Key 未存储) 表单提交、数据修改
PUT 否 否 永不命中 资源覆盖、文件上传
规范参考: HTTP RFC 7234 明确规定,POST 和 PUT 的响应不能被缓存系统自动使用,除非响应中包含显式的 Expires 或 Cache-Control: max-age,但 OkHttp 内置 Cache 类只处理 GET 的缓存逻辑。

4.3 缓存键生成实现流程

以下流程展示了 OkHttp 实际生成缓存键的步骤:

  1. 标准化请求 URL:去除片段(#fragment)、对字符进行规范编码。
  2. 提取方法名:只处理 GET;其他方法直接返回 null,不触发缓存键生成。
  3. 拼接基础字符串:scheme + "://" + host + ":" + port + path + "?" + encodedQuery。
  4. 计算 MD5:MessageDigest.getInstance("MD5").digest(data) 得到 16 字节。
  5. 格式化为 32 位小写十六进制字符串,作为磁盘文件名后缀。

4.4 代码示例:查看缓存键与缓存行为

以下 Java 代码演示了如何手动获取 OkHttp 生成的缓存键,并验证不同方法的行为:

import okhttp3.*;
import java.security.MessageDigest;

public class CacheKeyDemo {
    public static void main(String[] args) throws Exception {
        OkHttpClient client = new OkHttpClient.Builder()
                .cache(new Cache(new File("./cache"), 10 * 1024 * 1024))
                .build();

        String url = "https://api.example.com/v1/user?id=42";

        // 创建 GET 请求
        Request getRequest = new Request.Builder().url(url).get().build();
        // 创建 POST 请求
        Request postRequest = new Request.Builder().url(url).post(RequestBody.create("{}", MediaType.get("application/json"))).build();

        System.out.println("GET cache key: " + cacheKeyForRequest(getRequest));
        System.out.println("POST cache key: " + cacheKeyForRequest(postRequest));

        // 执行请求并观察缓存文件
        Response getResp = client.newCall(getRequest).execute();
        getResp.close();
        Response postResp = client.newCall(postRequest).execute();
        postResp.close();

        // 查看磁盘:只有 GET 响应会被存储,后缀名即为缓存键
        File cacheDir = new File("./cache");
        File[] files = cacheDir.listFiles((dir, name) -> name.endsWith(".0") || name.endsWith(".1"));
        System.out.println("Disk cache files count: " + (files != null ? files.length : 0));
    }

    static String cacheKeyForRequest(Request request) {
        if (!request.method().equals("GET")) {
            return "(non-GET – no cache key)";
        }
        // 以下模拟 OkHttp 内部逻辑(简化)
        try {
            String raw = request.url().toString().replace("#", "");
            MessageDigest digest = MessageDigest.getInstance("MD5");
            byte[] hash = digest.digest(raw.getBytes("UTF-8"));
            StringBuilder hex = new StringBuilder();
            for (byte b : hash) {
                hex.append(String.format("%02x", b));
            }
            return hex.toString();
        } catch (Exception e) {
            return "(error)";
        }
    }
}

4.5 调试与部署注意事项

  • 调试缓存键:启用 OkHttp 的日志拦截器(HttpLoggingInterceptor),观察 Cache: miss / hit 状态。
  • 部署检查:确保服务端对 GET 请求返回正确的 Cache-Control 头部;对 POST/PUT 请求不要期望客户端缓存。
  • 文件系统清理:缓存文件以 <key>.0(元数据)和 <key>.1(响应体)存储,清理时需删除成对文件。

4.6 常见错误与性能优化

错误场景 原因 解决方案
POST 响应被意外缓存 使用了非标准缓存策略或自定义拦截器 检查代码,确保只有 GET 请求进入 OkHttp 的 Cache 模块
相同 URL 的 GET 与 POST 冲突 缓存键仅基于 URL,未区分方法 OkHttp 内部已过滤,无需担心;自定义缓存时注意方法判读
缓存键过长导致文件系统错误 URL 包含大量查询参数或中文编码 OkHttp 已使用 MD5 缩短,无需额外处理
性能优化建议: 如果业务中确实需要缓存 POST 或 PUT 的响应(例如幂等查询类 POST),请使用自定义拦截器手动实现缓存,不要依赖 OkHttp 的 Cache 类,避免打破 HTTP 语义并引起调试混乱。
缓存键生成与请求方法行为流程图 HTTP 请求到达 方法是否为 GET? 标准化 URL → MD5 → 缓存键 磁盘缓存(.0 + .1 文件) 无缓存键,直接请求 响应不被缓存 关键含义 GET 请求会被提取缓存键并读写磁盘;POST 和 PUT 直接跳过缓存机制,不生成键、不存储响应。 是 否(POST / PUT / 其他)

4.7 小结

缓存键由请求 URL 经 MD5 生成,且 OkHttp 只对 GET 响应进行缓存。POST 和 PUT 的响应默认不会被存储或命中。开发中应遵循 HTTP 语义,仅在 GET 请求上依赖 OkHttp 的缓存模块,从而获得可靠、可预测的离线缓存行为。