一、引言
在现代 Android 和 Java 开发中,网络请求是一项非常常见的操作。OkHttp 作为一个高效的 HTTP 客户端,被广泛应用于各种项目中。OkHttp 的拦截器机制是其核心特性之一,它允许开发者在网络请求的不同阶段进行拦截和处理,从而实现诸如日志记录、缓存处理、请求头添加等功能。本文将深入探讨 OkHttp 拦截器的原理,并通过代码示例详细展示其实现过程。
二、OkHttp 简介
OkHttp 是一个由 Square 公司开发的开源 HTTP 客户端,它具有高效、可靠、支持 HTTP/2 和 SPDY 等特性。OkHttp 的设计理念是简洁、灵活,并且提供了丰富的 API,使得开发者可以方便地进行网络请求的发送和处理。
2.1 OkHttp 的基本使用
以下是一个简单的使用 OkHttp 发送 GET 请求的示例代码:
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class OkHttpExample {
public static void main(String[] args) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://www.example.com")
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String responseData = response.body().string();
System.out.println(responseData);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述代码中,我们首先创建了一个OkHttpClient实例,然后构建了一个Request对象,指定了请求的 URL。最后,通过client.newCall(request).execute()方法发送请求并获取响应。
三、OkHttp 拦截器概述
3.1 拦截器的作用
拦截器是 OkHttp 提供的一种机制,用于在网络请求的不同阶段对请求和响应进行拦截和处理。通过拦截器,开发者可以实现以下功能:
- 日志记录:记录请求和响应的详细信息,方便调试和监控。
- 缓存处理:实现请求的缓存策略,减少不必要的网络请求。
- 请求头添加:在请求中添加必要的请求头,如用户认证信息、设备信息等。
- 重试机制:在请求失败时进行重试,提高请求的成功率。
3.2 拦截器的类型
OkHttp 中的拦截器分为两种类型:
- 应用拦截器(Application Interceptor) :应用拦截器是在请求被发送到网络之前和响应返回给应用程序之后进行拦截的。它只关心应用程序发起的原始请求和最终的响应,不关心中间的重定向、重试等过程。
- 网络拦截器(Network Interceptor) :网络拦截器是在请求真正被发送到网络和从网络接收到响应时进行拦截的。它可以看到请求和响应的所有细节,包括重定向、重试等中间过程。
四、OkHttp 拦截器的原理
4.1 责任链模式
OkHttp 的拦截器机制基于责任链模式(Chain of Responsibility Pattern)。责任链模式是一种行为设计模式,它允许你将请求沿着处理者链进行传递,直到有一个处理者能够处理该请求为止。在 OkHttp 中,每个拦截器都是一个处理者,请求会依次经过每个拦截器进行处理,最终到达网络或返回给应用程序。
4.2 拦截器链的构建
当我们使用OkHttpClient发送请求时,OkHttp 会自动构建一个拦截器链。拦截器链的顺序如下:
- 应用拦截器:按照添加的顺序依次执行。
- 重试和重定向拦截器(RetryAndFollowUpInterceptor) :处理请求的重试和重定向逻辑。
- 桥接拦截器(BridgeInterceptor) :将应用程序的请求转换为网络请求,添加必要的请求头。
- 缓存拦截器(CacheInterceptor) :检查缓存,如果缓存命中则直接返回缓存响应,否则继续发送网络请求。
- 连接拦截器(ConnectInterceptor) :建立与服务器的连接。
- 网络拦截器:按照添加的顺序依次执行。
- CallServerInterceptor:向服务器发送请求并接收响应。
4.3 拦截器的执行过程
当一个请求被发送时,它会从拦截器链的头部开始,依次经过每个拦截器。每个拦截器都有机会对请求进行修改、处理或直接返回响应。如果一个拦截器返回了响应,则请求处理过程结束,响应会沿着拦截器链反向返回给应用程序。如果一个拦截器没有返回响应,则会调用下一个拦截器的intercept方法继续处理请求。
五、OkHttp 拦截器的实现
5.1 自定义应用拦截器
以下是一个自定义应用拦截器的示例代码,用于记录请求和响应的日志:
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class LoggingInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long t1 = System.nanoTime();
System.out.println(String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));
Response response = chain.proceed(request);
long t2 = System.nanoTime();
System.out.println(String.format("Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6d, response.headers()));
return response;
}
}
在上述代码中,我们实现了Interceptor接口,并重写了intercept方法。在intercept方法中,我们首先记录了请求的信息,然后调用chain.proceed(request)方法将请求传递给下一个拦截器,并获取响应。最后,我们记录了响应的信息并返回响应。
5.2 自定义网络拦截器
以下是一个自定义网络拦截器的示例代码,用于在请求中添加自定义请求头:
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class HeaderInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request newRequest = originalRequest.newBuilder()
.header("Custom-Header", "Custom-Value")
.build();
return chain.proceed(newRequest);
}
在上述代码中,我们同样实现了Interceptor接口,并重写了intercept方法。在intercept方法中,我们首先获取原始请求,然后创建一个新的请求,并在新请求中添加了自定义请求头。最后,我们调用chain.proceed(newRequest)方法将新请求传递给下一个拦截器,并返回响应。
5.3 使用自定义拦截器
以下是如何使用自定义拦截器的示例代码:
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class InterceptorExample {
public static void main(String[] args) {
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.addNetworkInterceptor(new HeaderInterceptor())
.build();
Request request = new Request.Builder()
.url("https://www.example.com")
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String responseData = response.body().string();
System.out.println(responseData);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述代码中,我们使用OkHttpClient.Builder创建了一个OkHttpClient实例,并通过addInterceptor方法添加了自定义应用拦截器,通过addNetworkInterceptor方法添加了自定义网络拦截器。然后,我们构建了一个请求并发送,最后处理响应。
六、深入分析拦截器的细节
6.1 拦截器的顺序
拦截器的顺序非常重要,因为它会影响请求和响应的处理流程。应用拦截器会在请求被发送到网络之前和响应返回给应用程序之后执行,而网络拦截器会在请求真正被发送到网络和从网络接收到响应时执行。因此,应用拦截器通常用于处理应用程序级别的逻辑,而网络拦截器用于处理网络级别的逻辑。
6.2 拦截器的异常处理
在拦截器的intercept方法中,可能会抛出IOException异常。开发者需要根据具体情况进行异常处理,例如在重试和重定向拦截器中,会对请求失败的情况进行重试。以下是一个简单的异常处理示例:
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class ExceptionHandlingInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
try {
return chain.proceed(request);
} catch (IOException e) {
System.out.println("Request failed: " + e.getMessage());
throw e;
}
}
}
在上述代码中,我们在intercept方法中捕获了IOException异常,并记录了异常信息,然后重新抛出异常,以便后续处理。
6.3. 应用拦截器(Application Interceptor)
虽然它并非严格意义上的内置拦截器(需开发者手动添加),但在整个请求处理流程中处于靠前位置,有必要首先介绍。
作用
应用拦截器主要用于处理应用程序级别的逻辑,它只关心应用程序发起的原始请求和最终的响应,不关心中间的重定向、重试等过程。
示例代码及分析
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class CustomApplicationInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// 可以在这里对请求进行修改,比如添加自定义请求头
Request newRequest = request.newBuilder()
.header("Custom-Header", "Custom-Value")
.build();
// 调用下一个拦截器处理请求
Response response = chain.proceed(newRequest);
return response;
}
}
在这个示例中,我们创建了一个自定义的应用拦截器,在请求被发送之前添加了一个自定义的请求头。当应用程序发起请求时,这个拦截器会首先对请求进行处理,然后将修改后的请求传递给下一个拦截器。
6.4 重试和重定向拦截器(RetryAndFollowUpInterceptor)
作用
该拦截器负责处理请求的重试和重定向逻辑。当请求失败或者服务器返回重定向响应时,它会根据相应的规则进行重试或者重定向操作。
源码关键逻辑分析
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Transmitter transmitter = realChain.transmitter();
int followUpCount = 0;
Response priorResponse = null;
while (true) {
transmitter.prepareToConnect(request);
try {
Response response = realChain.proceed(request);
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
Request followUp = followUpRequest(response, route);
if (followUp == null) {
if (!forWebSocket) {
transmitter.releaseConnectionNoEvents();
}
return response;
}
RequestBody followUpBody = followUp.body();
if (followUpBody != null && followUpBody.isOneShot()) {
return response;
}
closeQuietly(response.body());
if (++followUpCount > MAX_FOLLOW_UPS) {
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
request = followUp;
priorResponse = response;
} catch (RouteException e) {
// 处理路由异常,可能会进行重试
if (!recover(e.getLastConnectException(), transmitter, false, request)) {
throw e.getFirstConnectException();
}
} catch (IOException e) {
// 处理 IO 异常,可能会进行重试
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, transmitter, requestSendStarted, request)) {
throw e;
}
}
}
}
在这个源码片段中,while (true) 循环用于处理重试和重定向操作。followUpRequest 方法会根据响应的状态码判断是否需要进行重定向,如果需要则返回一个新的请求。当遇到路由异常或 IO 异常时,recover 方法会根据具体情况判断是否可以进行重试。
6.3 桥接拦截器(BridgeInterceptor)
作用
桥接拦截器的主要作用是将应用程序的请求转换为网络请求,添加必要的请求头,如 Content-Type、Content-Length、User-Agent 等。
源码关键逻辑分析
public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
if (body != null) {
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}
// 添加其他必要的请求头
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies));
}
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
Response networkResponse = chain.proceed(requestBuilder.build());
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
String contentType = networkResponse.header("Content-Type");
responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
}
return responseBuilder.build();
}
6.4 缓存拦截器(CacheInterceptor)
作用
缓存拦截器用于检查缓存,如果缓存命中则直接返回缓存响应,否则继续发送网络请求,并将响应结果缓存起来。
源码关键逻辑分析
public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// 如果不需要网络请求,直接返回缓存响应
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// 如果缓存响应和网络响应都存在,进行合并处理
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
在这个源码中,首先通过 cache.get 方法尝试从缓存中获取响应。然后根据 CacheStrategy 判断是否需要发送网络请求。如果缓存命中且不需要网络请求,直接返回缓存响应;如果需要网络请求,发送请求并获取网络响应。最后,根据响应结果更新缓存。
6.5 连接拦截器(ConnectInterceptor)
作用
连接拦截器负责建立与服务器的连接,包括选择合适的路由、建立 TCP 连接、进行 TLS 握手等操作。
源码关键逻辑分析
public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
Transmitter transmitter = realChain.transmitter();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
return realChain.proceed(request, transmitter, exchange);
}
在这个源码中,transmitter.newExchange 方法会建立与服务器的连接,并创建一个 Exchange 对象,用于处理请求和响应的交换。然后将请求、Transmitter 和 Exchange 对象传递给下一个拦截器继续处理。
6.6 网络拦截器(Network Interceptor)
网络拦截器和应用拦截器类似,可由开发者手动添加,也可在 OkHttp 内部使用。它会在请求真正被发送到网络和从网络接收到响应时进行拦截。
作用
网络拦截器可以看到请求和响应的所有细节,包括重定向、重试等中间过程,常用于处理网络级别的逻辑,如日志记录、网络监控等。
示例代码及分析
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class CustomNetworkInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// 可以在这里记录请求信息
System.out.println("Sending network request: " + request.url());
Response response = chain.proceed(request);
// 可以在这里记录响应信息
System.out.println("Received network response: " + response.code());
return response;
}
}
在这个示例中,我们创建了一个自定义的网络拦截器,在请求被发送到网络之前和从网络接收到响应之后分别记录了请求和响应的信息。
6.7 CallServerInterceptor
作用
CallServerInterceptor 是拦截器链中的最后一个拦截器,它负责向服务器发送请求并接收响应。
源码关键逻辑分析
public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Exchange exchange = realChain.exchange();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
exchange.writeRequestHeaders(request);
boolean responseHeadersStarted = false;
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
// Continue" response before transmitting the request body. If we don't get that, return
// what we did get (such as a 4xx response) without ever transmitting the request body.
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
exchange.flushRequest();
responseHeadersStarted = true;
exchange.responseHeadersStart();
responseBuilder = exchange.readResponseHeaders(true);
}
if (responseBuilder == null) {
// Write the request body if the "Expect: 100-continue" expectation was met.
Sink requestBodyOut = exchange.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
} else {
exchange.noRequestBody();
if (!exchange.connection().isMultiplexed()) {
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
// from being reused. Otherwise we're still obligated to transmit the request body to
// leave the connection in a consistent state.
exchange.noNewExchangesOnConnection();
}
}
} else {
exchange.noRequestBody();
}
if (request.body() == null || !request.body().isOneShot()) {
exchange.finishRequest();
}
if (!responseHeadersStarted) {
exchange.responseHeadersStart();
}
if (responseBuilder == null) {
responseBuilder = exchange.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(exchange.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
if (code == 100) {
// server sent a 100-continue even though we did not request one.
// try again to read the actual response
response = exchange.readResponseHeaders(false)
.request(request)
.handshake(exchange.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
exchange.responseHeadersEnd(response);
if (forWebSocket && code == 101) {
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(exchange.openResponseBody(response))
.build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
exchange.noNewExchangesOnConnection();
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;
}
在这个源码中,CallServerInterceptor 首先向服务器发送请求头,如果请求有请求体,会根据情况发送请求体。然后接收服务器的响应头和响应体,并构建最终的响应对象返回。
通过对这些内置拦截器的深入分析,我们可以更好地理解 OkHttp 的工作原理,并且能够根据实际需求灵活使用和扩展拦截器机制。
七、总结
OkHttp 的拦截器机制是一个非常强大和灵活的特性,它基于责任链模式,允许开发者在网络请求的不同阶段对请求和响应进行拦截和处理。通过自定义拦截器,开发者可以实现日志记录、缓存处理、请求头添加等功能,提高应用程序的可维护性和扩展性。在使用拦截器时,需要注意拦截器的顺序、异常处理和性能优化等问题。
以上就是关于 OkHttp 拦截器的原理和实现的详细介绍,希望通过本文的学习,你能够更好地理解和使用 OkHttp 的拦截器机制。