OkHttp源码分析(三)RetryAndFollowUpInterceptor

1,736 阅读6分钟

RetryAndFollowUpInterceptor是OKHttp的第一个框架提供的拦截器,看变量名称,主要负责重试和重定向工作。

为什么第一个拦截器要负责这方面的工作呢?有何用意?

我们想一下拦截器的整体运行模式,类似递归一条链,从从上往下执行每一个链结拦截器后得到的结果又会反方向的在链上传递结果,那么这时路过的每一个拦截器都有权利对执行的结果进行处理,可以装饰这个结果,可以抛弃这个结果,也可以重新执行后面的链结。
RetryAndFollowUpInterceptor作为最后一个拦截器,执行成功与否的结果都会返回到他的拦截器中。相当于一个兜底策略,请求失败的都会返回到这个拦截器中进行处理。

graph TD
拦截器1 --> 拦截器2
拦截器2 --> 拦截器1
拦截器2 --> 拦截器3
拦截器3 --> 拦截器2
拦截器3 --> 拦截器4
拦截器4 --> 拦截器3

使用位置

第一个拦截器是哪儿个呢?看下面的拦截器链的配置代码,是外部对OKHttpClient设置的interceptors,通过addInterceptor进行配置。说明这个拦截器是最底部的拦截器。我们可以在整个网络请求完成后,选择进行的操作,比如重新请求网络,或者配置请求回来的网络数据,可以说非常的自由。权限非常大。
比它权限还大的,就是自己定义的拦截器,我们可以通过配置这个变量,完成自己的配置工作。灵活度很高。

List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors()); // 自己定义的拦截器
interceptors.add(new RetryAndFollowUpInterceptor(client));// 重试和重定向拦截器
interceptors.add(new BridgeInterceptor(client.cookieJar()));
。。。

关于RetryAndFollowUpInterceptor我们先看下他的创建和使用再分析他的功能:重试和重定向。

创建和使用

RetryAndFollowUpInterceptor的创建在RealCall创建时就创建出来了,而不是像其他拦截器在创建拦截链时才创建出来。为什么要提前进行创建呢。

  1. RetryAndFollowUpInterceptor提供了一个cancel的取消方法,调用RealCall的cancel会到RetryAndFollowUpInterceptor中执行cancel。
  2. RetryAndFollowUpInterceptor提供了一个captureCallStackTrace方法,通过这个方法可以设置堆栈信息,因为RetryAndFollowUpInterceptor是第一个拦截器,相当于排头兵,链上的初始信息都要通过他进行设置。 上面的两方面原因导致RetryAndFollowUpInterceptor需要提前创建出来。
    使用方面就比较简单,直接插入到了拦截链中,等待拦截链运行。

运行

拦截器中具体运行的拦截代码在intercept(Chain chain)方法中,这个方法是通过RealInterceptorChain#process()方法调用进来。intercept(Chain chain)方法会传入下一个RealInterceptorChain链结,内部再调用这个链结的process()继续运行下一个链结,以达到拦截链的运行效果。依次往复。

RetryAndFollowUpInterceptor

@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 = 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 {
      //执行下一个链
      response = realChain.proceed(request, streamAllocation, null, null);
      releaseConnection = false;
    } catch (RouteException e) {
        // 在异常捕获中,判断是否可以重试
    } finally {
      if (releaseConnection) {
        streamAllocation.streamFailed(null);
        streamAllocation.release();
      }
    }

    Request followUp;
    try {
      //获取重定向请求
      followUp = followUpRequest(response, streamAllocation.route());
    } catch (IOException e) {
      streamAllocation.release();
      throw e;
    }

    if (followUp == null) {
      //不是重定向响应,直接返回结果
      streamAllocation.release();
      return response;
    }

    //配置重定向请求
    。。。
  }
}

RetryAndFollowUpInterceptorintercept(Chain chain)方法中的逻辑比较清晰,先创建了一个StreamAllocation,这个类非常重要,后面我们会单独讲这个类。请求的处理和重试重定向的处理在一个无限的while循环内部,因为重试和重定向的次数可能很多。接下来通过传入的下一个链结,执行realChain.proceed(request, streamAllocation, null, null)运行下面拦截器链的逻辑,这里是一个同步操作,直接等待整个链执行完成,获取最终的response。而重试的逻辑就在异常的捕获里处理。重定向的逻辑主要通过followUpRequest方法获取重定向新的请求Resquest。
下面分别讲下两个部分。

Retry重试

重试的逻辑主要在异常捕获里。RouteException和IOException异常,又通过recover方法看书否可以进行重试,如果可以重试就执行continue,因为时无限循环,执行下一个循环。重新执行realChain.proceed,也就是重新请求网络,这就是重试的主要逻辑。

try {
  response = realChain.proceed(request, streamAllocation, null, null);
  releaseConnection = false;
} catch (RouteException e) {
  // 因为路由失败,请求可能还没有发出
  if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
    throw e.getFirstConnectException();
  }
  releaseConnection = false;
  continue;
} catch (IOException e) {
  // 可能与服务器通信失败,请求可能已经发出了
  boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
  if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
  releaseConnection = false;
  continue;
}

是否可以重试的逻辑在recover方法中,RouteException和IOException异常中,不同的参数为第三个参数,表示请求是否已经开始,如果抛出RouteException,那么请求肯定还没有开始,如果抛出的是IOException,并且不是ConnectionShutdownException类型,表示请求已经开始了。
下面具体看下recover方法。如果可以恢复就返回true,如果不能恢复就返回false。

private boolean recover(IOException e, StreamAllocation streamAllocation,
    boolean requestSendStarted, Request userRequest) {
  streamAllocation.streamFailed(e);

  if (!client.retryOnConnectionFailure()) return false;

  if (requestSendStarted && requestIsUnrepeatable(e, userRequest)) return false;

  if (!isRecoverable(e, requestSendStarted)) return false;

  // 没有更多的路线不可以尝试。
  if (!streamAllocation.hasMoreRoutes()) return false;
    
  // 可以恢复
  return true;
}
  1. 如果在OkHttpClient中设置了retryOnConnectionFailure字段为false,表示在错误时不重试。这个第一篇提到过过。
  2. 如果请求已经发出了,但是这个请求不能重复请求,那么就不能重试。什么情况下这个请求不能重复请求,在requestIsUnrepeatable方法中,如果body是UnrepeatableRequestBody或者异常是FileNotFoundException,都不能重复请求。
    private boolean requestIsUnrepeatable(IOException e, Request userRequest) {
          return userRequest.body() instanceof UnrepeatableRequestBody
          || e instanceof FileNotFoundException;
    }
    
  3. 通过isRecoverable方法判断是否可以恢复。返回true表示可以重试
    private boolean isRecoverable(IOException e, boolean requestSendStarted) {
      // 如果存在协议问题,不要恢复。
      if (e instanceof ProtocolException) {
        return false;
      }
    
      // 如果出现中断,只在请求没有开始并且出现SocketTimeoutException,套接字读取/接受时间超时才可以重试。
      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. 如果当前没有更多路由可用了,streamAllocation.hasMoreRoutes()返回false。就不可重试。 以上就是重试的具体逻辑,通过recover方法判断可以重试,如果可以就继续执行下面的逻辑,如果不可以就直接抛出异常,结束这次的请求。

FollowUp重定向

Request followUp;
try {
  followUp = followUpRequest(response, streamAllocation.route());
} catch (IOException e) {
  streamAllocation.release();
  throw e;
}

if (followUp == null) {
  streamAllocation.release();
  return response;
}

closeQuietly(response.body());

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());
}

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;

FollowUp重定向的请求获取通过followUp = followUpRequest(response, streamAllocation.route())进行配置,返回202的成功请求也会走这个逻辑。如果能够获取到重定向请求,那么表示服务端需要我们做重定向处理。如果获取不到合格请求,那么就是正确的返回,直接return返回正确的response。获取到重定向请求后,还会对这个请求做处理。整体逻辑就是这三个部分。我们逐一分析。

获取重定向请求

通过followUpRequest方法获取,逻辑大体就是匹配重定向的Http错误码进行处理。重定向的错误码都是以3打头的。

private Request followUpRequest(Response userResponse, Route route) throws IOException {
  if (userResponse == null) throw new IllegalStateException();
  int responseCode = userResponse.code();

  final String method = userResponse.request().method();
  switch (responseCode) {
    case HTTP_PROXY_AUTH:
    。。。
    case HTTP_UNAUTHORIZED:
      。。。
    case HTTP_PERM_REDIRECT:
    case HTTP_TEMP_REDIRECT:
      。。。
    case HTTP_MULT_CHOICE:
    case HTTP_MOVED_PERM:
    case HTTP_MOVED_TEMP:
    case HTTP_SEE_OTHER:
      。。。
    default:
      return null;
  }
}

上面这段代码比较长,可以看出不光处理了3开头的重定向状态码,还处理了407、401等错误。我们逐一分析下。

  1. HTTP_PROXY_AUTH 407
    表示代理服务器需要我们的认证

    Proxy selectedProxy = route.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);
    

    处理的代码如上所述,如果此时的代理类型不是Http,这时可能是用了其他类型的代理比如SOCKS或者没有代理,这时会抛出一个异常。如果当前是合法的,那么就会通过proxyAuthenticator().authenticate进行认证。proxyAuthenticator如果我们不进行设置,那么就是空的,如果需要认证需要实现Authenticator,并重写authenticate。 进行认证操作。

  2. HTTP_UNAUTHORIZED 401
    表示服务器需要我们进行认证,这里直接通过authenticator().authenticate进行认证,authenticator的默认实现也是一个空实现,如果需要进行认证,需要实现Authenticator,并重写authenticate。和上面的操作一致。

    client.authenticator().authenticate(route, userResponse);
    
  3. HTTP_PERM_REDIRECT 308 HTTP_TEMP_REDIRECT 307
    HTTP_MULT_CHOICE 306 HTTP_MOVED_PERM 305 HTTP_MOVED_TEMP 304 HTTP_SEE_OTHER 303
    上面这些就是重定向的错误码了。
    首先307和308有点特殊,如果收到307或308状态代码以响应 ,并且请求方式是除GET或HEAD以外的请求,则用户代理不得自动重定向请求。

    case HTTP_PERM_REDIRECT:
    case HTTP_TEMP_REDIRECT:
      if (!method.equals("GET") && !method.equals("HEAD")) {
        return null;
      }
    

    重定向请求的具体逻辑放在后面

  4. HTTP_CLIENT_TIMEOUT 408
    408 在实践中很少见,但一些服务器(如 HAProxy)使用此响应代码。 规范说我们可以不加修改地重复请求。现代浏览器也重复请求(即使是非幂等的)。

  5. HTTP_UNAVAILABLE 503
    503是一种HTTP协议的服务器端错误状态代码,它表示服务器尚未处于可以接受请求的状态。

    if (userResponse.priorResponse() != null
    && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
      // 如果上次的响应也是同样的错误码,那么就放弃,不在请求
      return null;
    }
    
    if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
      // specifically received an instruction to retry without delay
      return userResponse.request();
    }
    
    return null;
    

    首先如果上次返回的响应错误码也是503,那么就不重复请求了。下面通过retryAfter方法判断header的Retry-After字段,这个字段表示服务器要求的下次请求的间隔时间,如果这个值是0,那么就会返回上一个请求,并重试这个请求。

    private int retryAfter(Response userResponse, int defaultDelay) {
      String header = userResponse.header("Retry-After");
    
          if (header == null) {
        return defaultDelay;
      }
    
      if (header.matches("\d+")) {
        return Integer.valueOf(header);
      }
    
      return Integer.MAX_VALUE;
    }
    

下面具体看下3开头的重定向错误码的具体处理。

if (!client.followRedirects()) return null;

String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
if (url == null) return null;

boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
if (!sameScheme && !client.followSslRedirects()) return null;

Request.Builder requestBuilder = userResponse.request().newBuilder();
if (HttpMethod.permitsRequestBody(method)) {
  final boolean maintainBody = HttpMethod.redirectsWithBody(method);
  if (HttpMethod.redirectsToGet(method)) {
    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");
  }
}

if (!sameConnection(userResponse, url)) {
  requestBuilder.removeHeader("Authorization");
}

return requestBuilder.url(url).build();

整体前面的几行做了拦截,这些情况不能进行重定向,后面通过了拦截,就具体构造重定向的请求了。

创建拦截阶段
  1. 如果client.followRedirects()为false不能重定向,也就是说在OKHttpClient中配置followRedirects为false,表示不允许重定向,默认是true。
  2. 重定向的response都会有一个Location,表示重定向的目的地。如果没有这个Location或者Location的内部是空的,都会非法的,不能重定向。
  3. 如果当前重定向的地址和原有请求的地址的scheme不同,也就是说一个是http一个是https。这时重定向就跨协议了。在OKHttpClient中,也有一个配置的参数表示是否可以进行这种重定向,就是followSSLRedirects,默认是true,如果我们设置成false,表示不能进行这样的重定向。
真正创建阶段

重定向的请求也是建立在原有请求基础上的,通过Request.Builder requestBuilder = userResponse.request().newBuilder()复制了一个新的Request。后面根据请求的method重新配置这个request。

  1. 如果当前的请求不是Get,我们需要重新设置下Request的body
  2. 除了PROPFIND请求外,其他都重定向为Get请求。PROPFIND类型的请求会带上返回的response的body。
  3. PROPFIND其他的类型,需要清除Transfer-Encoding、Content-Length、Content-Type等标记
  4. 如果sameConnection返回false,也就是如果是跨主机的请求,需要清除header中认证的标记。

上面就是重定向的具体逻辑在OkHttp中的实现。

下一篇讲BridgeInterceptor,框架提供的第二个拦截器