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 实际生成缓存键的步骤:
- 标准化请求 URL:去除片段(#fragment)、对字符进行规范编码。
- 提取方法名:只处理
GET;其他方法直接返回null,不触发缓存键生成。 - 拼接基础字符串:
scheme + "://" + host + ":" + port + path + "?" + encodedQuery。 - 计算 MD5:
MessageDigest.getInstance("MD5").digest(data)得到 16 字节。 - 格式化为 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 语义并引起调试混乱。
4.7 小结
缓存键由请求 URL 经 MD5 生成,且 OkHttp 只对 GET 响应进行缓存。POST 和 PUT 的响应默认不会被存储或命中。开发中应遵循 HTTP 语义,仅在 GET 请求上依赖 OkHttp 的缓存模块,从而获得可靠、可预测的离线缓存行为。