第2章 OkHttp拦截器体系与执行顺序

在深入Retrofit埋点之前,必须掌握其底层网络组件OkHttp的拦截器体系。拦截器是OkHttp请求/响应的核心扩展点,理解每种拦截器的定位、执行顺序及适用场景,是实现精准、高效网络埋点的前提。

2.1 拦截器体系结构

OkHttp共内置四类拦截器,按其注册位置与职责分为两层:Application拦截器在链路最外层,Network拦截器在最内层;中间由重试重定向拦截器和Bridge拦截器串联。整体执行流程如下SVG所示。

关键原则:拦截器按添加顺序构成责任链,每个拦截器可决定是否继续传递请求,或直接返回响应。Application拦截器对一次请求仅触发一次,而Network拦截器可能因重试或重定向被调用多次。
OkHttp拦截器执行顺序(一次典型请求) Application 拦截器 重试重定向拦截器 RetryAndFollowUpInterceptor Bridge 拦截器 BridgeInterceptor Network 拦截器 请求方向 应用层 → 网络层 响应方向 网络层 → 应用层 注意:Network拦截器在重试/重定向时被多次调用,Application拦截器只调用一次

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();
日志输出示例:
[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()或创建缓冲响应体。

2.7 小结