第2章 OkHttp拦截器体系与执行顺序
在深入Retrofit埋点之前,必须掌握其底层网络组件OkHttp的拦截器体系。拦截器是OkHttp请求/响应的核心扩展点,理解每种拦截器的定位、执行顺序及适用场景,是实现精准、高效网络埋点的前提。
2.1 拦截器体系结构
OkHttp共内置四类拦截器,按其注册位置与职责分为两层:Application拦截器在链路最外层,Network拦截器在最内层;中间由重试重定向拦截器和Bridge拦截器串联。整体执行流程如下SVG所示。
关键原则:拦截器按添加顺序构成责任链,每个拦截器可决定是否继续传递请求,或直接返回响应。Application拦截器对一次请求仅触发一次,而Network拦截器可能因重试或重定向被调用多次。
2.2 四种拦截器职责对比
| 拦截器类型 | 内置类名 | 添加方式 | 触发次数/请求 | 典型用途 |
|---|---|---|---|---|
| Application 拦截器 | (用户自定义) | addInterceptor() |
1次(忽略重试) | 全局埋点、认证、缓存控制 |
| 重试重定向拦截器 | RetryAndFollowUpInterceptor | 自动内置 | 内部多次(透明) | 自动重试、301/302跟随 |
| Bridge 拦截器 | BridgeInterceptor | 自动内置 | 1次(请求/响应适配) | 填充请求头(Cookie/Content-Length等) |
| Network 拦截器 | (用户自定义) | addNetworkInterceptor() |
每请求多次(含重试) | 网络层日志、重试次数统计、网关交互 |
2.3 拦截器注册与执行顺序验证
通过以下代码创建OkHttpClient并注册两类拦截器,观察日志输出顺序。
// Application拦截器
Interceptor appInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
System.out.println("[App] 请求前: " + chain.request().url());
Response response = chain.proceed(chain.request());
System.out.println("[App] 响应后: " + response.code());
return response;
}
};
// Network拦截器
Interceptor netInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
System.out.println("[Net] 请求前: " + chain.request().url());
Response response = chain.proceed(chain.request());
System.out.println("[Net] 响应后: " + response.code());
return response;
}
};
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(appInterceptor) // 最外层
.addNetworkInterceptor(netInterceptor) // 最内层
.build();
// 发起请求(可能触发重试)
client.newCall(new Request.Builder().url("http://httpbin.org/status/302").build()).execute();
日志输出示例:
现象:Application拦截器打印了两次请求(分别对应原始请求和重定向请求),但每次请求内只触发一次;Network拦截器同样分别打印两次,且顺序在外层拦截器之前输出(因为Network拦截器更靠近网络层)。
[App] 请求前: http://httpbin.org/status/302[App] 响应后: 302[App] 请求前: http://httpbin.org/redirect/1 (重定向后新请求)[App] 响应后: 200[Net] 请求前: http://httpbin.org/status/302[Net] 响应后: 302[Net] 请求前: http://httpbin.org/redirect/1[Net] 响应后: 200现象:Application拦截器打印了两次请求(分别对应原始请求和重定向请求),但每次请求内只触发一次;Network拦截器同样分别打印两次,且顺序在外层拦截器之前输出(因为Network拦截器更靠近网络层)。
2.4 埋点场景中的选择策略
- 业务埋点(用户行为统计):使用Application拦截器,保证无论内部发生多少次重试,只记录一次业务事件。
- 网络性能监控(重试次数、首字节时间):使用Network拦截器,准确获取每次真实网络请求的耗时与状态码。
- Cookie/Header管理:使用Bridge拦截器之前的Application拦截器进行全局注入,避免与Bridge逻辑冲突。
2.5 常见错误与避坑
| 错误场景 | 原因 | 正确做法 |
|---|---|---|
| Network拦截器中解析响应体导致流消耗 | OkHttp响应体只能被读取一次 | 使用peekBody()或克隆请求/响应 |
| Application拦截器中修改请求URL导致重定向失效 | 修改请求后重试拦截器基于原始请求跟随 | 只在Network拦截器中修改网络层参数 |
| 在Application拦截器中记录“请求”但未记录“重定向” | 混淆了业务请求与网络请求 | 按埋点需求选择正确层级的拦截器 |
| 拦截器内执行耗时操作(如数据库写入) | 阻塞OkHttp线程池 | 使用异步任务或仅记录轻量日志 |
2.6 性能与稳定性优化要点
- 避免在网络拦截器做线程阻塞操作:Network拦截器运行在OkHttp的IO线程池,阻塞会显著降低并发吞吐。
- 使用无锁数据结构记录埋点:如
ConcurrentLinkedQueue或RingBuffer,防止高并发时锁竞争。 - 控制拦截器链长度:每个拦截器增加一次链调用开销,建议Application拦截器不超过3个,Network拦截器不超过2个。
- 利用链路追踪ID关联多次网络请求:在Application拦截器中生成唯一请求ID,传递到Network拦截器用于聚合重试数据。
警告:不要在Application拦截器中直接获取完整响应体(
response.body().string()),这会导致原始响应体被消费,后续Bridge/Network拦截器无法正常工作。如需读取,应使用peekBody()或创建缓冲响应体。