本文主要介绍了Okhttp拦截器Interceptor,以及如何自定义拦截器,因本人英语能力有限,如有错误的地方,请参考原文
Interceptors(拦截器)
拦截器是一种强大的机制能够监控,重写,重试网络请求。下面是一个日志拦截器的例子,它的功能是在控制台输出发出去的请求和收到响应。
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;
}
}
调用chain.proceed(request)是每一个拦截器实现的核心方法,这个看起来很简单的方法是所有HTTP工作发生的地方,即产生满足于请求的响应;
拦截器可以连续调用,假设你有一个压缩拦截器和一个校验拦截器,那么你需要决定数据是先压缩再校验还是先校验再压缩;OkHttp使用列表来跟踪拦截器,并按顺序调用拦截器。
下图是经典的拦截器工作原理图
拦截器分为两类应用拦截器和网络拦截器
应用拦截器
拦截器可以被注册为应用拦截器和网络拦截器,下面我们使用上面已经定义好的LoggingInterceptor
来说明应用拦截器和网络拦截器的不同;
首先在构建OkHttpClient时(OkHttpClient.Builder)通过调用addInterceptor()来注册应用拦截器:
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();
response.body().close();
URL http://www.publicobject.com/helloworld.txt应被重定向(可以理解为原地址的内容迁移到了新地址)到URL
https://www.publicobject.com/helloworld.txt,OkHttp会自动完成重定向,我们的应用程序拦截器被调用一次,从chain.proceed()返回的响应是重定向后的响应。下面是日志拦截器输出的结果:
//这是发出的请求Request的信息,可以看到请求地址是http://www.publicobject.com/helloworld.txt
INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example
//这是响应的结果信息,可以看到结果响应自https://publicobject.com/helloworld.txt
//很明显响应的结果是来自重定向之后的结果而不是原始地址返回的结果
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
我们可以看到请求自动重定向了,因为response.request().url()(响应结果的地址)和request.url()(发出请求的地址)是不同的。
网络拦截器
注册网络拦截器和注册应用拦截器很相似调用addNetworkInterceptor()来代替调用addInterceptor()就可以了。
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();
response.body().close();
当我们运行代码之后,拦截器会执行两次,一次是初始请求http://www.publicobject.com/helloworld.txt一次是重定向之后的请求
https://publicobject.com/helloworld.txt我们可以看一下日志拦截器输出的日志:
//发出初始请求的请求信息 url为http://www.publicobject.com/helloworld.txt
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
//原始请求http://www.publicobject.com/helloworld.txt的响应结果
//这个响应头中Location对应的值就是重定向地址
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
//重定向后发出的请求 url为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
//重定向后的地址https://publicobject.com/helloworld.txt响应的结果
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
网络拦截器包含了更多的数据,例如Accept-Encoding: gzip头信息用于通知支持响应压缩,网络拦截器的链是非空连接,可用于询问用于连接到Web服务器的IP地址和TLS配置。
对比应用拦截器和网络拦截器
通过对比日志拦截器输出的结果,很明显,网络拦截器执行的次数大于应用拦截器,应用拦截器只关心发出的初始请求和最终响应的结果,而不关心中间过程,比如重定向;二网络拦截器会监视每一次请求和每一次响应,包括原始请求和中间请求。
各自的优点:
应用拦截器的优点:
1,不需要关注中间响应,比如重定向响应和重试响应;
2,总是会调用一次,即使是从缓存当中获取响应;
3,关注应用程序的初始意图。不关注OkHttp注入的Headers,如If-None-Match;
4,允许短路和不调用Chain.proceed();
5,允许重试并多次调用Chain.proceed()。
网络拦截器的优点:
1,能够操作中间响应,如重定向响应和重试响应;
2,当网络短路的时候不会调用缓存的响应;
3,关注每一次通过网络传输的数据;
4,会访问带有请求的连接。
拦截器的应用
1,重写请求
拦截器可以添加、移除、替换请求头。也可以改造只有一个请求体的请求的请求体(这句话优点拗口),例如你可以添加一个用于压缩请求体的应用拦截器,前提是你的web服务器支持请求体的压缩;
/** 这个拦截器会压缩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();
}
};
}
}
2,重写响应
对应的,拦截器可以重写响应头并转换响应体。这通常比重写请求头更危险,因为它可能违反了web服务器的期望。如果您处于棘手的情况并准备应对后果,重写响应标头是解决问题的有效方法。例如,您可以修复服务器配置错误的Cache-Control响应头,以实现更好的响应缓存。
/** 一个危险的拦截器,用于重写服务器的缓存响应头 */
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服务器上的相应修补程序时效果最佳!