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

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


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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容