文章目录
- 1. 拦截器
- 1. 拦截器链
- 2. 实际案例
- 1. 注册为应用拦截器
- 2. 注册为网络拦截器
- 3. 如何选择用哪种拦截器
- 1. 应用拦截器
- 2. 网络层拦截器
- 3. 重写请求
- 4. 重写响应
- 4. 可用性
- 2. 事件监听器
- 1. 请求的生命周期
- 2. EventListener使用案例
- 3. EventListener.Factory
- 4. 调用失败的请求
1. 拦截器
拦截器是一种强大的机制,可以用来监测、重写、重试调用。下面是一个简单的例子,用来打印请求的输入和输出。
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;}
}
1. 拦截器链
拦截器可以形成一个拦截器链, 拦截器分为应用层拦截器、网络层拦截器,如下图所示:
2. 实际案例
假设我们访问的是http://www.publicobject.com/helloworld.txt,它实际上会有一个302跳转到https://publicobject.com/helloworld.txt, OkHttp会自动完成跳转。
我们用上面的LoggingInterceptor做为例子来讲解应用拦截器和网络层拦截器的区别。
1. 注册为应用拦截器
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();
输出只会有一个Request,一个Response,内部的跳转过程没有感知。以下是输出内容:
INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp ExampleINFO: 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
2. 注册为网络拦截器
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();
输出会感知每一个Request、Response对象。以下为输出内容:
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: gzipINFO: 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.txtINFO: 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: gzipINFO: 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、Connection这些OkHttp帮我们自动添加的头信息。
3. 如何选择用哪种拦截器
1. 应用拦截器
- 不关心重定向、重试
- 永远只显示一次,即使是从缓存中读取结果
- 只关心应用的原始意图。不关心OkHttp自动添加的头,比如If-None-Match等
- 允许短路,不调用Chain.process(request)
- 允许重试,调用多次Chain.process(request)
2. 网络层拦截器
- 允许修改和操作中间的请求结果和状态
- 从缓存中读取时,不调用拦截器
- 关心中间过程
- 访问请求的Connection对象
3. 重写请求
拦截器可以添加、删除、修改HTTP头,甚至可以改变请求体。比如你可以在拦截器内对请求体做压缩(如果你知道Web服务器支持的话)。
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();}};}
}
4. 重写响应
重写响应可以重写响应的HTTP头、响应内容等,一般来说是不推荐的,可能会违反直觉。
比如为响应自动添加缓存头。
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();}
};
4. 可用性
需要okhttp 2.2以后的版本,不可以和OkUrlFactory一起工作,不能在Retrofit 1.8及以下版本使用。
2. 事件监听器
你可以通过事件知道OkHttp的内部运行状态。通过时间我们可以监控:
- HTTP请求的大小、频率
- 这些请求对应的网络性能
(这里提及的API还不是最终版的API,OkHttp 3.9里这个API只是非稳定的预览版,预计在3.10、3.11会稳定)
你可以通过继承EventListener并覆盖你感兴趣的事件方法来获取通知。
1. 请求的生命周期
一次普通的请求完成,会触发以下事件:
2. EventListener使用案例
class PrintingEventListener extends EventListener {private long callStartNanos;private void printEvent(String name) {long nowNanos = System.nanoTime();if (name.equals("callStart")) {callStartNanos = nowNanos;}long elapsedNanos = nowNanos - callStartNanos;System.out.printf("%.3f %s%n", elapsedNanos / 1000000000d, name);}@Override public void callStart(Call call) {printEvent("callStart");}@Override public void callEnd(Call call) {printEvent("callEnd");}@Override public void dnsStart(Call call, String domainName) {printEvent("dnsStart");}@Override public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {printEvent("dnsEnd");}...
}
EventListener会被所有Call共享,所以EventListener本身不能是有状态的,如果需要的话,采用EventListener.Factory
3. EventListener.Factory
Factory可以让相同的Call共用一个EventListener对象,也可以只是随机选一部分Call来监听
class MetricsEventListener extends EventListener {private static final Factory FACTORY = new Factory() {@Override public EventListener create(Call call) {if (Math.random() < 0.10) {return new MetricsEventListener(call);} else {return EventListener.NONE;}}};...
}
4. 调用失败的请求
如果是连接阶段,触发事件connectFailed(),否则触发callFailed()。失败发生的时候,有可能存在调用了start方法,但是没有调用end方法的情况。