2. OkHttp 核心架构与请求拦截器机制:Interceptor 链、RealCall、责任链模式
OkHttp 的请求执行并非一个简单的函数调用,而是由 RealCall 实例协调一组 Interceptor 按照顺序逐个处理,最终到达网络层。这种设计完全遵循 责任链(Chain of Responsibility) 模式,使每个拦截器只关心自己的职责,并在不修改核心执行流程的情况下实现插拔式扩展。对于安全防护来说,拦截器链是插入身份校验、加密、日志埋点、证书绑定等逻辑的唯一正确位置。
2.1 从 RealCall 到请求执行
当调用 OkHttpClient.newCall(request) 时,OkHttp 返回一个 RealCall 实例。RealCall 持有 OkHttpClient 和原始 Request,其内部通过 Dispatcher 决定立即执行还是排队,最终调用 getResponseWithInterceptorChain() 构建拦截器链并开始执行。
| 组件 | 职责 |
|---|---|
| RealCall | 封装一次 HTTP 请求的全生命周期;提供 execute()(同步)和 enqueue()(异步)入口 |
| Dispatcher | 管理异步请求的并发度(最大 64 个并发,默认每个 Host 最大 5 个) |
| Interceptor.Chain | 责任链的纽带;每个拦截器通过 chain 获取 Request 并调用 proceed() 将请求传递到下一拦截器 |
// RealCall 核心执行路径(简化)
Response getResponseWithInterceptorChain() throws IOException {
List<Interceptor> interceptors = new ArrayList<>();
// 用户自定义应用拦截器
interceptors.addAll(client.interceptors());
// OkHttp 内置拦截器
interceptors.add(new RetryAndFollowUpInterceptor(client));
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.cache()));
interceptors.add(new ConnectInterceptor(client));
// 网络拦截器(用户自定义)
if (client.networkInterceptors() != null)
interceptors.addAll(client.networkInterceptors());
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest,
this, client, callTimeout, readTimeout, writeTimeout);
return chain.proceed(originalRequest);
}
2.2 Interceptor 链与责任链模式
责任链模式在此场景的含义是:每个拦截器都可以决定是否继续传递请求(短路),或者在响应返回时对 Response 进行后处理。 拦截器接口只有两个方法:
public interface Interceptor {
Response intercept(Chain chain) throws IOException;
interface Chain {
Request request();
Response proceed(Request request) throws IOException;
// 连接、超时等获取方法
Connection connection();
Call call();
int connectTimeoutMillis();
// …
}
}
每个拦截器内部的典型实现模式:
- 读取
chain.request()得到当前请求。 - 对请求进行前置处理(例如添加 Header、加密 Body)。
- 调用
chain.proceed(modifiedRequest)将请求传递给下一个拦截器。 - 对返回的
Response进行后置处理(例如解密 Body、校验签名)。 - 返回最终 Response。
关键安全知识:应用拦截器(Application Interceptor)在重定向和重试之前执行一次;网络拦截器(Network Interceptor)在每次真实的网络请求前后执行,适合做请求/响应的篡改监测。安全拦截器通常注册为应用拦截器,避免重复执行。
API 对比:
client.interceptors().add() 添加应用拦截器;client.networkInterceptors().add() 添加网络拦截器。两种拦截器的执行时机和次数不同,务必根据安全需求选择。
2.3 Interceptor 链的结构图
下图清晰展示了从 RealCall 到网络请求的完整拦截器责任链,以及每个阶段的处理边界。
2.4 拦截器编写与 API 实操
自定义拦截器需要实现 Interceptor 接口。以下是一个完整的安全拦截器示例,它在请求头中加入认证 ,在响应中校验签名。
public class AuthSignatureInterceptor implements Interceptor {
private static final Charset UTF8 = Charset.forName("UTF-8");
private final String ;
public AuthSignatureInterceptor(String ) {
this. = ;
}
@Override
public Response intercept(Chain chain) throws IOException {
// 1. 前置处理:读取原始请求,添加身份头
Request original = chain.request();
Request.Builder builder = original.newBuilder()
.header("X-", )
.header("X-Timestamp", String.valueOf(System.currentTimeMillis()));
// 2. 对 Body 进行摘要签名(示例仅对字符串 Body 签名)
RequestBody body = original.body();
if (body != null && body.contentLength() > 0) {
Buffer buffer = new Buffer();
body.writeTo(buffer);
String bodyString = buffer.readString(UTF8);
String signature = HmacSha256.sign(bodyString, );
builder.header("X-Signature", signature);
}
Request signedRequest = builder.build();
// 3. 传递给下一拦截器
Response response = chain.proceed(signedRequest);
// 4. 后置处理:校验响应签名
String respSignature = response.header("X-Response-Signature");
if (respSignature != null) {
ResponseBody respBody = response.body();
if (respBody != null) {
String respStr = respBody.string();
String expected = HmacSha256.sign(respStr, );
if (!expected.equals(respSignature)) {
throw new IOException("响应签名验证失败,数据可能被篡改");
}
}
}
return response;
}
}
注册方式:
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new AuthSignatureInterceptor("sk-xxxx"))
.addInterceptor(new LoggingInterceptor()) // 调试日志
.build();
2.5 调试与验证
在拦截器内部打印日志是最直接的调试手段。使用 Logging Interceptor 可观察每个拦截器对请求/响应的修改效果:
// 内置日志拦截器(需要手动注册)
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
client = client.newBuilder()
.addInterceptor(loggingInterceptor) // 应用拦截器级别
.build();
调试技巧:在开发阶段将
LoggingInterceptor 置于拦截器链最前面,可查看原始请求;置于最后面(网络拦截器位置)可查看最终发往服务器的请求和返回的原始响应。
2.6 常见错误与避坑
| 错误场景 | 原因 | 解决 |
|---|---|---|
拦截器中多次调用 chain.proceed() | 违反责任链约定,抛出 IllegalStateException | 确保每个拦截器只调用一次 proceed() |
| 忘记关闭 Body 导致连接泄漏 | 后置处理中调用了 body.string() 后未重新包装 | 使用 ResponseBody.create() 重新构建响应 |
| 拦截器中抛出 RuntimeException | 导致整个请求失败且无法重试 | 拦截器内使用 try-catch 处理安全异常,转为 IOException |
| 拦截器顺序混乱导致安全逻辑被跳过 | 自定义拦截器未加到最前面或被内置拦截器覆盖 | 安全拦截器优先注册,放在应用拦截器列表头部 |
性能警告:每个拦截器的
intercept() 方法都在 OkHttp 的调度线程中执行。如果安全拦截器中包含阻塞操作(如网络校验、文件 I/O),必须使用异步方式或交给后台线程,否则会阻塞整个请求池。
2.7 性能与稳定性优化建议
- 减少 Body 读取次数:签名验证时尽量读取一次 Body,使用
BufferedSource边读取边计算。 - 缓存签名结果:对于相同请求体在短时间内不应重复计算签名。
- 超时分离:关键安全拦截器可单独设置更短的超时,避免被慢请求拖死。
- 使用 Network Interceptor 做无状态监控:重试场景下网络拦截器会多次执行,适合记录网络耗时,但不宜做重操作。
// 优化示例:在 ConnectInterceptor 后使用专用超时
OkHttpClient safeClient = client.newBuilder()
.addInterceptor(chain -> {
Request request = chain.request()
.newBuilder()
.header("X-Debug", "1")
.build();
return chain.withConnectTimeout(3, TimeUnit.SECONDS).proceed(request);
})
.build();