RetryAndFollowUpInterceptor是什么
从名字上就能看出,这个拦截器的作用是用来重试和重定向的,上期分析出拦截器中主要用来执行的方法是intercept()方法,那就废话不多说直接看RetryAndFollowUpInterceptor
在intercept()中做了什么。
看源码
public final class RetryAndFollowUpInterceptor implements Interceptor {
...
@Override public Response intercept(Chain chain) throws IOException {
...
int followUpCount = 0;
Response priorResponse = null;
while (true) {
...
try {
response = realChain.proceed(request, transmitter, null);//代码1
success = true;
} catch (RouteException e) {//代码2
if (!recover(e.getLastConnectException(), transmitter, false, request)) {//代码7
throw e.getFirstConnectException();//代码4
}
continue;
} catch (IOException e) {//代码3
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, transmitter, requestSendStarted, request)) throw e;//代码5
continue;
}
...
Request followUp = followUpRequest(response, route);//代码16
...
if (++followUpCount > MAX_FOLLOW_UPS) {//代码6
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
...
}
}
...
}
上述就是主要代码,其实也就只做了两件事情
- 重试
- 重定向
重试
首先明确一点,为什么重试?首先肯定是程序员要求你这么做,如果说不需要重试那肯定就不用去重试了。其次肯定是因为超时等原因,我们的网络连接失败而抛出异常。那每个异常都要重试吗?并不是,如果说你压根连host的格式都不对,这样的重试肯定毫无意义。基于以上两点RetryAndFollowUpInterceptor
拦截器为我们做了一些处理。
代码1
执行后,如果抛出异常就说明可能需要重试,如果没有必要重试,拦截器会直接抛出异常,就像代码4
和代码5
那样,如果有必要重试,就会继续往下走。
整个重试和重定向的逻辑都是在while循环里的,所以满足条件的重试肯定不会一直循环下去,看代码6
中会有一个MAX_FOLLOW_UPS
,这个的意思是允许最大重试的次数,这个次数是20,如果重试次数大于了20次,也会报错ProtocolException
,定为20的原因如下:
/**
* How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,
* curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
*/
private static final int MAX_FOLLOW_UPS = 20;
意思是,我们这个20次重试不是瞎搞的,这是参考了Chrome
、Firefox
、Safari
这几个大佬开发的浏览器的重试次数综合考虑后得来的。
那怎么样的重试才是值得去重试的呢?我们看代码7
和代码5
中,在Catch后都会执行一个recover()
方法,这个方法的返回值直接决定了是不是要直接报错(跳出循环,不再重试),接下来看看这个方法究竟干了什么:
public final class RetryAndFollowUpInterceptor implements Interceptor {
private boolean recover(IOException e, Transmitter transmitter,
boolean requestSendStarted, Request userRequest) {
// The application layer has forbidden retries.
if (!client.retryOnConnectionFailure()) return false;//代码8
// We can't send the request body again.
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;//代码9
// This exception is fatal.
if (!isRecoverable(e, requestSendStarted)) return false;//代码10
// No more routes to attempt.
if (!transmitter.canRetry()) return false;//代码11
// For failure recovery, use the same route selector with a new connection.
return true;
}
}
简单来说这个方法的意义就是判断当前是否值得去重试,如果值得重试就返回true,如果不值得重试就返回false,其中一共有4种判断
代码8
的意思是如果程序员不让我重试,那我就不重试
代码9
的意思是同一个请求(request
)不能被调用两次,如果请求是一次性的并且已经发送过了(requestSendStarted==true
),那就不允许重试了
代码10
的意思是出现了严重的问题,我们跟进去看看
public final class RetryAndFollowUpInterceptor implements Interceptor {
...
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
if (e instanceof ProtocolException) {//代码12
return false;
}
if (e instanceof InterruptedIOException) {//代码13
return e instanceof SocketTimeoutException && !requestSendStarted;
}
if (e instanceof SSLHandshakeException) {//代码14
if (e.getCause() instanceof CertificateException) {
return false;
}
}
if (e instanceof SSLPeerUnverifiedException) {//代码15
return false;
}
return true;
}
...
}
代码12
意思是如果是协议错误,那不能重试,代码13
如果不是超时异常那也不能重试,代码14
ssl握手异常不能重试,代码15
ssl握手未授权异常,不能重试
简而言之,就是那种很严重的错误,你重试100遍都没结果的,索性就不要重试。
代码11
意思是如果没有可供重试的路由那也不能重试。
重试的判断基本上就是这些逻辑,接下来我们回过去看看代码16
的重定向的代码
重定向
public final class RetryAndFollowUpInterceptor implements Interceptor {
...
private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
if (userResponse == null) throw new IllegalStateException();
int responseCode = userResponse.code();//代码17
final String method = userResponse.request().method();
switch (responseCode) {
case HTTP_PROXY_AUTH://代码18
Proxy selectedProxy = route != null
? route.proxy()
: client.proxy();
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
}
return client.proxyAuthenticator().authenticate(route, userResponse);
case HTTP_UNAUTHORIZED://代码19
return client.authenticator().authenticate(route, userResponse);
case HTTP_PERM_REDIRECT:
case HTTP_TEMP_REDIRECT:
// "If the 307 or 308 status code is received in response to a request other than GET
// or HEAD, the user agent MUST NOT automatically redirect the request"
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;//代码20
}
// fall-through
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
// Does the client allow redirects?
if (!client.followRedirects()) return null;//代码21
String location = userResponse.header("Location");//代码22
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
// Don't follow redirects to unsupported protocols.
if (url == null) return null;
// If configured, don't follow redirects between SSL and non-SSL.
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());//代码23
if (!sameScheme && !client.followSslRedirects()) return null;//代码24
// Most redirects don't include a request body.
Request.Builder requestBuilder = userResponse.request().newBuilder();
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method);//代码25
if (HttpMethod.redirectsToGet(method)) {//代码26
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
if (!sameConnection(userResponse.request().url(), url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
case HTTP_CLIENT_TIMEOUT:
// 408's are rare in practice, but some servers like HAProxy use this response code. The
// spec says that we may repeat the request without modifications. Modern browsers also
// repeat the request (even non-idempotent ones.)
if (!client.retryOnConnectionFailure()) {
// The application layer has directed us not to retry the request.
return null;
}
RequestBody requestBody = userResponse.request().body();
if (requestBody != null && requestBody.isOneShot()) {
return null;
}
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
// We attempted to retry and got another timeout. Give up.
return null;
}
if (retryAfter(userResponse, 0) > 0) {
return null;
}
return userResponse.request();
case HTTP_UNAVAILABLE:
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
// We attempted to retry and got another timeout. Give up.
return null;
}
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
// specifically received an instruction to retry without delay
return userResponse.request();
}
return null;
default:
return null;
}
}
...
}
代码走到这里后其实服务端已经返回的数据,这段代码的目的就是看看是否还需要重定向,如果不需要重定向就返回null
代码17
拿到的其实就是服务器返回的状态码
代码18
就是看有没有代理,并返回
代码19
需要身份验证的
代码20
意思是如果返回的状态码为永久或临时重定向的,而重定向的方法不是GET或者HEAD的就不用管
代码21
如果用户不让重定向就不用管
代码22
是取出重定向的地址,如果是null则不管,如果不为null就直接通过HttpUrl url = userResponse.request().url().resolve(location);
拼接成一个httpUrl
代码23
判断当前重定向是否是http和https之间的重定向,而代码24
表示如果确实是http和https之间的重定向,那就得看用户是否让这种重定向,如果不让那就返回null不用管
代码25
的方法是判断这个方法既不是GET也不是HEAD
代码26
是判断改方法是不是不等于PROPFIND
重定向的判断大致就是这么多
再回过去看看,这个拦截器其实主要的工作都集中在了判断上面,判断是否能重试,判断是否能重定向,而每个判断又分为是否是用户(程序员)不让重试或者重定向,还是因为外在条件导致报错。