第3章:添加自定义应用拦截器
应用拦截器(Application Interceptor)是OkHttp拦截器体系中最靠近调用方的一层。本章将围绕缓存头部注入、条件请求处理、响应缓存策略判断以及拦截器链调用四个核心环节,构建一个可复用的自定义应用拦截器。你将在理解拦截器调用模型的基础上,完成从代码编写到调试验证的完整路径。
学习目标
· 理解 Application Interceptor 与 Network Interceptor 的差异
· 掌握 Request.Builder 添加缓存头部(Cache-Control、If-None-Match 等)的方法
· 实现条件请求(Conditional GET)的逻辑分支
· 编写响应缓存有效性判断(如 max-age、must-revalidate)
· 正确调用 chain.proceed() 并处理缓存写入
· 理解 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 调用链中的位置以及本章涉及的三个关键处理阶段:
3.2 实现流程
自定义应用拦截器的完整实现分为以下六个步骤:
- 创建拦截器类:实现
Interceptor接口,重写intercept()方法。 - 解析原始请求:从
Chain对象获取Request实例。 - 注入缓存头部:通过
Request.Builder添加Cache-Control和条件请求头。 - 条件请求处理:根据本地缓存元数据(如 ETag、Last-Modified)决定是否附加
If-None-Match或If-Modified-Since。 - 调用拦截器链:执行
chain.proceed(request)获取响应。 - 响应缓存策略判断:基于响应状态码和首部(
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 修改请求头部,处理条件请求,并在响应返回后根据缓存策略决定是否持久化。下一章将深入网络拦截器的缓存协作机制。