阅读过很多写过Okhttp原理的文章,笔者看完觉得还是很失望,因此大白君打算写一个okhttp系列,接下来下半年还会写一个对RxJava2的系列,看过我去年写Retrofit和Java系列的,都知道我是用心去写的,并不是凭空的去翻译API, 大都是自己亲自使用过后总结出的经验,再来一步步的构思去写,好的博客,笔者认为首先要构思清晰,由浅入深,再总结回顾,最后恍然大悟!这样才能让读者身临其境,喜欢的朋友可以继续关注Tamic,若是出,只出高质量技术文章
Java里的拦截器是动态拦截Action调用的对象。它提供了一种机制可以使开发者可以定义在一个action执行的前后执行的代码,也可以在一个Action执行前阻止其执行,同时也提供了一种可以提取action中可重用部分的方式。 在AOP(Aspect-Oriented Programming)中拦截器用于在某个方法或字段被访问之前,进行拦截然后在之前或之后加入某些操作。
过滤器可以简单理解为“取你所想取”,忽视掉那些你不想要的东西;拦截器可以简单理解为“拒你所想拒”,关心你想要拒绝掉哪些东西,比如一个BBS论坛上拦截掉敏感词汇。
Android里面过滤器大家用的已经无法再陌生了,Filter就是一个很好的列子,在清单文件注册Filter,就可以过滤掉启动某个组件的Action。
为何这里我额外的描述了过滤器的概念,后续文章中我会对okhttp的网络返回结果加一个自定义过滤器,用来做错误处理!以前文章我是通过rxjava的转换器实现的。
Okhttp拦截器因此应运而生,处理一次网络调用的Action拦截,做某些修改操作。下图是拦截器在okhttp中的介绍,图就直接安利了,来自github。
使用
okhttp拦截器用法很简单,构建OkHttpClient的时候通过.addInterceptor()就可以将拦截器加入到一次会话中。
OkHttpClient client =
new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.build();
自定义拦截器
拦截器是Okhttp一种强大的机制,可以监视,重写和重试每一次网络请求。下面是一个简单的拦截器,用于输出传出的请求和响应日志。
class LoggingInterceptor implements Interceptor{
@Override
public Responseintercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
long t1 = System.nanoTime();
Response response = chain.proceed(request);
long t2 = System.nanoTime();
response.request().url(), (t2 - t1) / 1e6d, response.headers()));
return response;
}
}
拦截器实现关键部分是调用chain.proceed(request)。这个方法是所有HTTP工作发生的地方,以满足请求和响应的需求。
拦截器可以有多个,假设同时拥有一个压缩拦截器和一个校验拦截器:需要确定数据是否已压缩,然后对数据进行校验,或者校验和压缩两者一起使用。OkHttp的拦截器用集合用来跟踪调度拦截器,拦截器是按集合索引按顺序调用。
应用拦截器
拦截器可以注册为应用程序或网络拦截器。使用LoggingInterceptor
来显示不同的地方。
注册一个应用程序通过调用拦截器的
addInterceptor()
上OkHttpClient.Builder
:
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();
URLhttp://www.publicobject.com/helloworld.txt
重定向到https://publicobject.com/helloworld.txt
,
OkHttp自动会跟踪这个重定向。应用拦截器被调用一次,网络返回响应chain.proceed()
具有重定向返回如下:
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
可以看到,被重定向是因为response.request().url()
不同于request.url()
。两个日志记录了两个不同的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(); 123456789101112123456789101112
跑起这个代码时,拦截器会运行两次。一次为初始请求http://www.publicobject.com/helloworld.txt
,另一个为重定向https://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
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
网络请求还包含了更多的数据,例如Accept-Encoding: gzip
由OkHttp添加的请求头来支持数据响应进行压缩。网络拦截器Chain具有非空值Connection(下文会讲到),可用于询问用于连接到Web服务器的IP地址和TLS配置。
在应用拦截器和网络拦截器之间如何让进行选择?先看看两个拦截器有不同的优点。
应用拦截器
拦截器可以添加,删除或替换请求头。还可以修改请求的正文。例如,如果连接到已知支持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();
}
};
}
}
对称地拦截器可以重写响应头并转换响应Body。这通常比重写请求头更加有杀伤管力,因为可以篡改,违反了web服务器的本身返回数据的本意!
在特殊的情况 需要修复容错服务端的返回的数据,重写返回的Head是解决问题的有效方式。例如,可以修复服务器配置了错误的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服务器上的不足,修复客户端程序数据的场景下效果更好!
1, Interceptor代码本质:
拦截器源码:包含基础的Request
和Response
获取接口,并且内部包含了Connection接口。 代码如下:
public interface Interceptor {
Response intercept(Chain chain) throws IOException;
interface Chain {
Request request();
Response proceed(Request request) throws IOException;
/**
* Returns the connection the request will be executed on. This is only available in the chains
* of network interceptors; for application interceptors this is always null.
*/
@Nullable Connection connection();
}
}
2 .Connection是神马东西?
Connection
是一次面向连接过程,这里包含基础的协议Protocol
, Socket
, Route
, Handshake.
`public interface Connection {
Route route();
Socket socket();
@Nullable
Handshake handshake();
Protocol protocol();
}
Handshake是ohkttp自己的握手机制,里面包括了SSL验证过程,这里不做源码分析,这里提供了基础Https的认证的基础根方法,本文不做探讨。
Interceptor怎么被调用
发起请求
OkHttpClient mOkHttpClient = new OkHttpClient();
Request request = new Request.Builder() .url("https://github.com/hongyangAndroid") .build();
//new call
Call call = mOkHttpClient.newCall(request);
进行newCaLL
RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
final EventListener.Factory eventListenerFactory = client.eventListenerFactory();
this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket;
//这里就是处理拦截器的地方!
this.retryAndFollowUpInterceptor = new
RetryAndFollowUpInterceptor(client, forWebSocket);
// TODO(jwilson):
this is unsafe publication and not threadsafe.
this.eventListener =
eventListenerFactory.create(this);
}
处理请求拦截
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
streamAllocation = new StreamAllocation(
client.connectionPool(),
createAddress(request.url()), callStackTrace); int followUpCount = 0;
Response priorResponse = null; while (true) { if (canceled) {
streamAllocation.release();
throw new IOException("Canceled"); }
Response response = null;
boolean releaseConnection = true;
try { //也就是这里进行上层注入的拦截拦截
response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
......
continue;
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
.......
}
Response proceed()方法很简单,内部使用集合进行遍历,一个反射进行真实数据处理! 其通过内部的
Response response = interceptor.intercept(next);
其实就回调到了你实现的intercept(Chain chain)
的接口,一次闭环结束!
处理返回拦截
使用者都知道我们每次进行一次请求调用call.execute() ,真正的response也在这里开始,拦截器也从这方法为导火索。
Override public Response execute() throws IOException { synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
} captureCallStackTrace();
try {
client.dispatcher().executed(this);
//处理拦截了
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} finally {
client.dispatcher().finished(this);
}
如果到这里你还未能猜出内部机制,这里也不用在介绍,通过处理请求拦截的介绍,你一也应该明白内部进行拦截器集合循环遍历,进行的具体处理。
到此明白了Interceptor的工作原理我们就可以愉快的使用他来完成一些功能了。
这里我做了一个图, 更能理解整个过程,只理解拦截机制,Okhttp源码流程带后续继续分析。
请看:http://www.jianshu.com/p/1a5f14b63f47
Okhttp对每个Request统一动态添加header和参数
请看 Rxjava +Retrofit 你需要掌握的几个技巧,Retrofit缓存。
OkHttp的拦截器需要OkHttp 2.2或以上版本使用。值得注意的是,拦截器不支持OkUrlFactory,或者依赖Okhttp的其他库,包括Retrofit≤1.8和 Picasso≤2.4。
如果看的不过瘾,下期到Tamic原创,分析下okhttp路由原理!
参考资料
Okhttp官方GitHub Wiki以及APi文档 Tamic / http://blog.csdn.net/sk719887916/article/details/74308343
iOS特殊赞助通道,随意支持作者!
☞ 持续关注,[撸框架系列]
后续不定期将推出撸RxJava, 撸RxBus,撸RxCache,RxLifecyle。
精彩推荐
技术 - 资讯 - 感悟
END