第3章 自定义OkHttp拦截器入门:Interceptor接口实现、chain.proceed()调用、修改Request/Response

3.1 拦截器核心原理:责任链模式

OkHttp 拦截器基于 责任链模式 设计。每个拦截器只处理自己负责的逻辑,然后将请求传递给下一个拦截器,最终到达服务器;响应则沿原路逆向返回。这种模式将请求处理与响应处理解耦,每个拦截器都可以独立修改 Request 和 Response。

核心认知: 拦截器是 OkHttp 网络请求的“管道”。你添加的每个拦截器都会在请求发送前和响应返回后获得执行机会,整个过程形成一个可插拔的链式结构。

拦截器责任链模式流程图 拦截器责任链:请求与响应双向流程 客户端 (请求发起) 拦截器 A (自定义·请求前) 拦截器 B (自定义·请求前) 服务器 (API 端点) Request → Request → Request → 服务器 (响应返回) 拦截器 B (自定义·响应后) 拦截器 A (自定义·响应后) 客户端 (接收响应) ← Response ← Response ← Response 请求向下传递,响应向上回溯 每个拦截器可双向修改

3.2 Interceptor 接口深入解析

OkHttp 的 Interceptor 接口位于 okhttp3.Interceptor 包中,是自定义拦截器的唯一契约。接口只定义了一个方法 intercept(Chain chain),所有拦截逻辑都在该方法中实现。

接口 / 内部接口 方法 参数 返回值 职责说明
Interceptor intercept() Chain Response 拦截并处理请求/响应,必须调用 chain.proceed()
Interceptor.Chain request() 无 Request 获取当前拦截器收到的请求对象
proceed() Request Response 将(可能已修改的)请求传递给下一个拦截器,返回响应
connection() 无 Connection? 获取当前请求的连接(可能为 null)

编码提示: Chain 对象是拦截器与责任链之间的桥梁。你只能通过 chain.request() 读取请求,通过 chain.proceed(request) 将请求传递给下一个拦截器。不要直接持有 Chain 实例跨越多次调用。

3.3 chain.proceed() 调用机制

chain.proceed() 是拦截器链的“发令枪”。调用该方法后,当前拦截器挂起执行,请求被传递给下一个拦截器(或最终的 OkHttp 网络层),直到响应返回。没有调用 proceed(),请求就会卡住,不会发送到服务器。

// chain.proceed() 的标准调用模式
@Override
public Response intercept(Chain chain) throws IOException {
    // 1. 获取原始请求
    Request request = chain.request();

    // 2. 可在此处修改请求(添加头、日志、加密等)

    // 3. 调用 proceed() 将请求传递给下一个拦截器
    Response response = chain.proceed(request);

    // 4. 可在此处修改响应(解密、统计、脱敏等)

    // 5. 返回最终响应
    return response;
}

3.4 修改 Request 与 Response

拦截器的核心价值在于可以在请求发送前和响应返回后插入自定义逻辑。修改时必须使用 Builder 模式创建不可变对象的新实例。

3.4.1 修改 Request

// 修改请求示例:添加鉴权头与请求 ID
@Override
public Response intercept(Chain chain) throws IOException {
    Request original = chain.request();

    // 构建新请求:添加请求头、修改 URL、替换 Body
    Request modified = original.newBuilder()
            .addHeader("Authorization", "Bearer " + authToken)
            .addHeader("X-Request-ID", generateUUID())
            .header("Accept-Encoding", "gzip")    // 覆盖已有头
            .method(original.method(), original.body()) // 保留原方法/Body
            .build();

    return chain.proceed(modified);
}

3.4.2 修改 Response

// 修改响应示例:添加埋点标记与解密 Body
@Override
public Response intercept(Chain chain) throws IOException {
    Response response = chain.proceed(chain.request());

    // 构建新响应:添加自定义头
    Response.Builder builder = response.newBuilder()
            .addHeader("X-Intercepted-Version", "1.0")
            .addHeader("X-Interceptor-Timestamp", String.valueOf(System.currentTimeMillis()));

    // 如果响应体需要解密/解压,创建新的 ResponseBody
    if (response.isSuccessful() && response.body() != null) {
        String decryptedBody = decrypt(response.body().string());
        builder.body(ResponseBody.create(
                decryptedBody,
                response.body().contentType()
        ));
    }

    return builder.build();
}
修改目标 Builder 方法 典型应用场景 注意事项
请求头 addHeader() / header() 鉴权、设备指纹、埋点 ID addHeader 追加,header 覆盖
请求 URL url() 动态域名、灰度路由 不要破坏 URL 编码结构与参数
请求 Body method() / post() 参数加密、请求体压缩 确保 Content-Type 与 Body 匹配
响应头 addHeader() / header() 埋点标记、调试信息、缓存控制 不要修改 Content-Length
响应体 body() 数据解密、解压、脱敏、转换 只能消费一次原 Body
状态码 code() 统一错误码转换 谨慎使用,避免语义错误

⚠ 高风险操作: 修改响应体时,原 response.body().string() 只能调用一次。调用后原 Body 流被关闭,务必创建新的 ResponseBody 对象并赋值给新 Response。多次消费同一 Body 会抛出 IllegalStateException。

3.5 拦截器注册与调试

自定义拦截器需要注册到 OkHttpClient 才能生效。OkHttp 提供两种注册方式:应用拦截器(addInterceptor())和 网络拦截器(addNetworkInterceptor())。本章只涉及应用拦截器。

// 注册自定义拦截器到 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new LoggingInterceptor())     // 应用拦截器
        .addInterceptor(new AuthInterceptor())   // 鉴权拦截器
        .connectTimeout(30, TimeUnit.SECONDS)
        .build();

// 使用该 client 发起请求时,拦截器自动生效
Request request = new Request.Builder()
        .url("https://api.example.com/data")
        .build();

client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        Log.e("Net", "请求失败: " + e.getMessage());
    }

    @Override
    public void onResponse(Call call, Response response) {
        Log.d("Net", "响应成功: " + response.code());
    }
});

3.5.1 调试日志输出

在开发阶段,可以在拦截器内部打印关键信息来验证拦截逻辑是否执行。注意不要在生产环境输出敏感数据。

// 调试日志拦截器示例
public class DebugLogInterceptor implements Interceptor {
    private static final String TAG = "OkHttp-Debug";

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        // 请求日志
        Log.d(TAG, "→ " + request.method() + " " + request.url());
        Log.d(TAG, "→ Headers: " + request.headers());

        long startNs = System.nanoTime();

        Response response = chain.proceed(request);

        long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);

        // 响应日志
        Log.d(TAG, "← " + response.code() + " " + response.message()
                + " (" + elapsedMs + "ms)");
        Log.d(TAG, "← Headers: " + response.headers());

        return response;
    }
}

调试技巧: 结合 Android Studio 的 Logcat 按 TAG 过滤输出,可以使用 Log.d("OkHttp-Debug", ...) 统一标签,方便快速定位拦截器日志。对于线上环境,建议将日志级别提升为 Log.w 或只在 Debug 构建中启用。

3.6 常见错误与最佳实践

3.6.1 必须调用 chain.proceed()

这是新手最常犯的错误。如果条件分支中遗漏了 chain.proceed(),请求将永远挂起,导致超时异常。

// ❌ 错误示例:条件分支遗漏 proceed()
@Override
public Response intercept(Chain chain) throws IOException {
    if (isNetworkAvailable()) {
        return chain.proceed(chain.request());
    }
    // 没有 else 分支调用 proceed(),请求卡死
    throw new IOException("No network");
}

// ✅ 正确做法:所有路径都必须调用 proceed() 或抛出异常
@Override
public Response intercept(Chain chain) throws IOException {
    if (isNetworkAvailable()) {
        return chain.proceed(chain.request());
    }
    // 使用默认缓存响应或明确抛出异常
    return buildOfflineResponse();
}

3.6.2 避免在 intercept() 中执行耗时操作

intercept() 方法在 OkHttp 的调度线程中执行,如果阻塞线程会拖慢整个请求队列。IO 密集型操作(如文件读写、加密)应在异步线程完成,或使用异步拦截器模式。

// ❌ 错误示例:在 intercept() 中做耗时加密
@Override
public Response intercept(Chain chain) throws IOException {
    Request req = chain.request();
    byte[] encrypted = heavyEncryption(req.body().bytes()); // 阻塞!
    return chain.proceed(req.newBuilder()
            .method(req.method(), RequestBody.create(encrypted, req.body().contentType()))
            .build());
}

// ✅ 正确做法:只做轻量操作,重操作放在业务层
@Override
public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    // 只添加标记头,不处理 Body
    Request tagged = request.newBuilder()
            .addHeader("X-Encrypt", "false")
            .build();
    return chain.proceed(tagged);
}

3.6.3 响应体只能消费一次

// ❌ 错误:多次读取 response.body().string()
Response response = chain.proceed(request);
String body1 = response.body().string();  // 第一次读取,Body 流关闭
String body2 = response.body().string();  // IllegalStateException!

// ✅ 正确:读取后创建新的 ResponseBody 再返回
Response response = chain.proceed(request);
String rawBody = response.body().string();
String processedBody = process(rawBody);
Response modified = response.newBuilder()
        .body(ResponseBody.create(processedBody, response.body().contentType()))
        .build();
return modified;

3.6.4 性能与稳定性优化清单

  • 轻量逻辑: intercept() 中只做 Header 修改、简单计数、日志拼接等 O(1) 操作。
  • 对象复用: 不要在每次拦截时创建大量临时对象,使用静态常量或对象池。
  • 异常处理: 拦截器中的异常会中断整个请求链,务必捕获预期异常并降级处理。
  • 线程安全: 拦截器实例会被多个请求共享,不要在拦截器内维护可变成员状态(如需统计,使用 AtomicLong 或线程安全集合)。
  • 拦截器顺序: addInterceptor() 的添加顺序就是执行顺序。先添加的拦截器更靠近客户端,后添加的更靠近服务器。

章节要点总结: 自定义 OkHttp 拦截器的本质是实现 Interceptor.intercept(Chain) 方法,通过 chain.proceed() 传递请求并接收响应,在链式调用前后分别修改 Request 和 Response。掌握这一模式就掌握了 OkHttp 埋点、鉴权、日志、加密等高级功能的基础。