OKhttp3拦截器——RetryAndFollowUpInterceptor

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

上述就是主要代码,其实也就只做了两件事情

  1. 重试
  2. 重定向

重试

首先明确一点,为什么重试?首先肯定是程序员要求你这么做,如果说不需要重试那肯定就不用去重试了。其次肯定是因为超时等原因,我们的网络连接失败而抛出异常。那每个异常都要重试吗?并不是,如果说你压根连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次重试不是瞎搞的,这是参考了ChromeFirefoxSafari这几个大佬开发的浏览器的重试次数综合考虑后得来的。

那怎么样的重试才是值得去重试的呢?我们看代码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如果不是超时异常那也不能重试,代码14ssl握手异常不能重试,代码15ssl握手未授权异常,不能重试

简而言之,就是那种很严重的错误,你重试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

重定向的判断大致就是这么多


再回过去看看,这个拦截器其实主要的工作都集中在了判断上面,判断是否能重试,判断是否能重定向,而每个判断又分为是否是用户(程序员)不让重试或者重定向,还是因为外在条件导致报错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值