Okhttp拦截器详解
Okhttp拦截器介绍
概念:拦截器是Okhttp
中提供的一种强大机制,它可以实现网络监听、请求以及响应重写、请求失败重试等功能。我们先来了解下Okhttp
中的系统拦截器:
- RetryAndFollowUpInterceptor:负责请求失败的时候实现重试重定向功能。
- BridgeInterceptor:将用户构造的请求转换为向服务器发送的请求,将服务器返回的响应转换为对用户友好的响应。
- CacheInterceptor:读取缓存、更新缓存。
- ConnectInterceptor:与服务器建立连接。
- CallServerInterceptor:从服务器读取响应。
1、拦截器的工作原理
在上一篇文章中我们提到了获取网络请求响应的核心是getResponseWithInterceptorChain()
方法,从方法名字也可以看出是通过拦截器链来获取响应,在Okhttp
中采用了责任链的设计模式来实现了拦截器链。它可以设置任意数量的Intercepter
来对网络请求及其响应做任何中间处理,比如设置缓存,Https
证书认证,统一对请求加密/防篡改社会,打印log
,过滤请求等等。
责任链模式:在责任链模式中,每一个对象和其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,客户只需要将请求发送到责任链即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。
接下来就通过getResponseWithInterceptorChain()
这个方法来具体了解一下:
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
//添加用户自定义的拦截器到拦截器链中,并在系统默认的拦截器之前执行
interceptors.addAll(client.interceptors());
//添加重试重定向拦截器
interceptors.add(retryAndFollowUpInterceptor);
//添加桥接拦截器,在此拦截器中默认添加许多请求头和解析响应头
interceptors.add(new BridgeInterceptor(client.cookieJar()));
//添加缓存拦截器,根据需要是否从缓存中返回响应
interceptors.add(new CacheInterceptor(client.internalCache()));
//添加连接拦截器
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
//发送请求,并获取响应数据
interceptors.add(new CallServerInterceptor(forWebSocket));
//创建拦截器链
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
//通过拦截器执行具体的请求
return chain.proceed(originalRequest);
}
从代码中可以看到首先创建了一个List
集合,泛型是Interceptor
,也就是拦截器,接着创建了一系列的系统拦截器(一开始介绍的五大拦截器)以及我们自定义的拦截器(client.interceptors()
和client.networkInterceptors()
),并添加到集合中,然后构建了拦截器链RealInterceptorChain
,最后通过执行拦截器链的proceed()
方法开始了获取服务器响应的整个流程。这个方法也是整个拦截器链的核心,接下来就看一下RealInterceptorChain
中的proceed()
方法。
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
......
// Call the next interceptor in the chain.
// 调用链中的下一个拦截器,index+1代表着下一个拦截器的索引
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
// 取出要调用的拦截器
Interceptor interceptor = interceptors.get(index);
// 调用每个拦截器的intercept方法
Response response = interceptor.intercept(next);
......
return response;
}
proceed()
方法的核心就是创建下一个拦截器。首先创建了一个拦截器,并且将index = index+1
,然后我们根据index
从存放拦截器的集合interceptors
中取出当前对应的拦截器,并且调用拦截器中的intercept()
方法。这样,当下一个拦截器希望自己的下一级继续处理这个请求的时候,可以调用传入的责任链的proceed()
方法。
2、RetryAndFollowUpInterceptor 重试重定向拦截器
RetryAndFollowUpInterceptor:重试重定向拦截器,负责在请求失败的时候重试以及重定向的自动后续请求。但并不是所有的请求失败都可以进行重连。
查看RetryAndFollowUpInterceptor
中的intercept
方法如下:
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Call call = realChain.call();
EventListener eventListener = realChain.eventListener();
// 创建streamAllocation
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
int followUpCount = 0;
Response priorResponse = null;
// 进入循环
while (true) {
// 判断是否取消请求
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;
boolean releaseConnection = true;
try {
// 【1】、将请求发给下一个拦截器,在执行的过程中可能会出现异常
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
// 【2】、路由连接失败,请求不会再次发送
// 在recover方法中会判断是否进行重试,如果不重试抛出异常
// 在一开始我们提到了并不是所有的失败都可以进行重连,具体哪些请求可以进行重连就在这个recover方法中。
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getFirstConnectException();
}
releaseConnection = false;
// 满足重试条件,继续重连
continue;
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
// 【3】、尝试与服务器通信失败,请求不会再次发送
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
// 满足重试条件,继续重连
continue;
} finally {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
// Attach the prior response if it exists. Such responses never have a body.
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
Request followUp;
try {
// 【4】、在followUpRequest中会判断是否需要重定向,如果需要重定向会返回一个Request用于重定向
followUp = followUpRequest(response, streamAllocation.route());
} catch (IOException e) {
streamAllocation.release();
throw e;
}
// followUp == null 表示不进行重定向,返回response
if (followUp == null) {
streamAllocation.release();
return response;
}
closeQuietly(response.body());
// 【5】、重定向次数最大为20次
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (followUp.body() instanceof UnrepeatableRequestBody) {
streamAllocation.release();
throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
}
// 重新创建StreamAllocation实例
if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(followUp.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
} else if (streamAllocation.codec() != null) {
throw new IllegalStateException("Closing the body of " + response
+ " didn't close its backing stream. Bad interceptor?");
}
// 重新赋值进行循环
request = followUp;
priorResponse = response;
}
}
类的介绍
StreamAllocation:维护了服务器连接、并发流和请求(Connection、Streams、Calls)
之间的关系,可以为一次请求寻找连接并建立流,从而完成远程通信。在当前的方法中没有用到,会把它们传到之后的拦截器来从服务器中获取请求的响应。
HttpCodec:定义了操作请求和解析响应的方法,实现类为Http1Codec
和Http2Codec
,分别对应Http1.x
和Http2
协议。
可以看到其实在RetryAndFollowUpInterceptor
中并没有对Request
请求做什么特殊的处理,就将请求发送给了下一个拦截器,在拿到后续的拦截器返回的Response
之后,RetryAndFollowUpInterceptor
主要是根据Response
的内容,以此来判断是否进行重试或者重定向的处理。
2.1 重试请求
根据【2】
和【3】
可以得出在请求期间如果发生了RouteException
或者IOException
会进行判断是否重新发起请求。而这两个异常都是根据recover()
来进行判断的,如果recover()
返回true
,就表示可以进行重试,那么我们就来看一下recover()
方法中做了哪些操作。
private boolean recover(IOException e, StreamAllocation streamAllocation,
boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
// 1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试
//The application layer has forbidden retries.
if (!client.retryOnConnectionFailure()) return false;
// 2、如果是RouteException,requestSendStarted这个值为false,无需关心
//We can't send the request body again.
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
return false;
//todo 3、判断是不是属于重试的异常
//This exception is fatal.
if (!isRecoverable(e, requestSendStarted)) return false;
//todo 4、不存在更多的路由
//No more routes to attempt.
if (!streamAllocation.hasMoreRoutes()) return false;
// For failure recovery, use the same route selector with a new connection.
return true;
}
1、我们可以在OkhttpClient
中配置是否允许进行重试,如果配置了不允许重试,那么请求发生异常后就不会进行重试的操作。
2、如果是RouteException
,requestSendStarted
这个值为false
,无需关心。 如果是IOException
,那么requestSendStarted
为false
的情况只有在http2
的io
异常的时候出现。那么我们来看第二个条件可以发现UnrepeatableRequestBody
是一个接口,这个条件表示如果我们自定义的请求body
实现了这个UnrepeatableRequestBody
这个接口的时候,就不进行重试请求。
3、判断是不是属于可以重试的异常,主要在isRecoverable()
中实现。
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
// 协议异常,ProtocolException异常的时候服务器不会返回内容,不能重试
// 请求和服务器的响应存在异常,没有按照http的协议来定义,重试也没用
if (e instanceof ProtocolException) {
return false;
}
// 如果是超时异常,可以进行重试
// 可能是发生了网络波动导致的Socket连接超时
if (e instanceof InterruptedIOException) {
return e instanceof SocketTimeoutException && !requestSendStarted;
}
// 证书不正确,有问题,不重试
if (e instanceof SSLHandshakeException) {
if (e.getCause() instanceof CertificateException) {
return false;
}
}
// 证书校验失败,不重试
if (e instanceof SSLPeerUnverifiedException) {
// e.g. a certificate pinning error.
return false;
}
return true;
}
4、检查当前有没有可用的路由