参考源码版本
okhttp-3.14.9
okhttp 是什么?一款封装 HTTP 协议的 HTTP 客户端。
拦截器是 okhttp 提供的一个强有力的工具,我们可以在请求前后做监控、请求/响应进行重写、失败重试等操作。
Okhttp 的拦截器采用了责任链模式,具体提供了两种拦截器:
我们通过实际的例子来看看两种的区别,方便起,实现一个公用拦截器:
class LoggingInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
long t1 = System.nanoTime();
logger.info(String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));
Response response = chain.proceed(request);
long t2 = System.nanoTime();
logger.info(String.format("Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6d, response.headers()));
return response;
}
}
从名字可以看出,这是一个记录日志的拦截器。
官方提供的测试网址,http://www.publicobject.com/helloworld.txt,当我们访问该网址的时候,会重定向到 https://publicobject.com/helloworld.txt 地址。
现在,我们通过 okhttp 请求来看看两种拦截器的差别。
这里通过 addInterceptor(…) 添加应用拦截器:
/**
* 应用层拦截器
*/
@Test
public void testApplicationInterceptors() throws IOException {
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
Objects.requireNonNull(response.body()).close();
}
我们看看处理结果:
INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example
INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
从结果可以看出,控制台主要输出了我们请求前和请求后的日志信息。
通过 addNetworkInterceptor(…) 添加网络拦截器:
/**
* 网络层拦截器
*/
@Test
public void testNetworkInterceptors() throws IOException {
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
Objects.requireNonNull(response.body()).close();
}
再来看看处理结果:
INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt
INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
你应该也看出差别了,这次控制台输出了两份 请求 + 响应 信息:
另外,输出里还包含了更多的网络层信息,比如 okhttp 添加的头信息:Accept-Encoding: gzip
两种拦截器都有各自的应用场景,我们该如何选择?
有了拦截器,我们可以对 http 请求头的参数新增、删除和覆盖等操作。
如果你觉得请求体过大,数据传输耗时较高的话,你可以对请求体进行压缩:
final class GzipRequestInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request originalRequest = chain.request();
if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
return chain.proceed(originalRequest);
}
Request compressedRequest = originalRequest.newBuilder()
.header("Content-Encoding", "gzip")
.method(originalRequest.method(), gzip(originalRequest.body()))
.build();
return chain.proceed(compressedRequest);
}
private RequestBody gzip(final RequestBody body) {
return new RequestBody() {
@Override public MediaType contentType() {
return body.contentType();
}
@Override public long contentLength() {
return -1; // We don't know the compressed length in advance!
}
@Override public void writeTo(BufferedSink sink) throws IOException {
BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
body.writeTo(gzipSink);
gzipSink.close();
}
};
}
}
案例中,我们覆盖了请求头参数 Content-Encoding:gzip
,也就是告诉服务器,这个一个压缩请求体,你需要用相同的方式来解压数据。
这通常比重写请求头更危险,因为它可能违反web服务器的期望!
当然,如果你处于一个棘手的情况,并准备处理结果,重写响应头是解决问题的一种强大方法。
例如,你可以修复服务器错误配置的 Cache-Control 响应头,以启用更好的响应缓存:
/** Dangerous interceptor that rewrites the server's cache-control header. */
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.header("Cache-Control", "max-age=60")
.build();
}
};
通常,当它补充了web服务器上的相应修复时,这种方法工作得最好!
老规矩,我们还是回归源码看看底层实现细节。
再回归下我们的测试案例:
/**
* 网络层拦截器
*/
@Test
public void testNetworkInterceptors() throws IOException {
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
// 执行 okhttp 请求
Response response = client.newCall(request).execute();
Objects.requireNonNull(response.body()).close();
}
入口 okhttp3.RealCall#execute:
可以看到,getResponseWithInterceptorChain
真正应用拦截器并执行 http 请求。
okhttp3.RealCall#getResponseWithInterceptorChain:
该方法主要做了两件大事:
chain
,并通过 chain.proceed(originalRequest) 执行到这里,其实已经可以解释很多现象了,比如应用拦截器与网络拦截器的核心区别是啥?为什么都是 Interceptor
的实现类,效果却有些不同?
你应该也发现本质原因了,主要区别在于拦截器装配的顺序不一样
。拦截器处理顺序:
在添加网络拦截器之前,添加了几个固定拦截器,比如 重试、桥接、缓存等拦截器。因此,网络层拦截器展示的效果与应用层拦截器不同,主要是由这几个拦截器在中间发挥作用。
我们再来回顾下这张图,你应该理解的更加深刻了:
值得注意的是,最后一个添加的拦截器是 CallServerInterceptor
,是真正执行 http 请求的拦截器。
换句话说,okhttp 将所有操作包括 执行 http 调用都封装为拦截器,然后通过拦截器链串联起来执行。
okhttp3.internal.http.RealInterceptorChain#proceed:
前面提到,拦截器是一个 List 集合,那么如何决定当前使用哪一个拦截器?
答案是:index
下标
原理是这样的:
index + 1
,即 执行下一个拦截器。进入我们自定义的拦截器:
至此,okhttp 请求处理流程梳理完毕!!!可以发现,okhttp 的核心链路是围绕着拦截器进行设计,我们可以实现任何自定义拦截器并添加至其中,非常方便。
本文带你一起探讨了 okhttp 拦截器的使用方法,及其底层处理原理,相信你也有了自己的想法。
拦截器的应用十分广泛,常见的还用 Spring 拦截器、Mybatis 拦截器以及 Feign 拦截器,相信你在开发过程中也遇到了很多。
你可以进一步探讨拦截器,多对比几款实现,看能否抽象出一款拦截器的通用理论?
- okhttp-interceptors#docs
- okhttp # github