RetryAndFollowUpInterceptor过滤器

356 阅读8分钟

RetryAndFollowUpInterceptor过滤器

    在上一篇笔记中梳理了OkHttp源码的大体执行流程,在HTTP请求中主要就是包含请求体的设置,请求数据的回调,请求数据的解析,线程的切换这些内容,其中主要分析了执行流程,包括:

  1. 创建OkHttpClient对象
  2. 创建Request对象
  3. 创建请求结果的回调,即Callback对象
  4. OkHttpClient配置请求体和请求回调
  5. 开启异步线程执行任务。

    同时,通过上一篇笔记也知道了,当请求数据的任务提交之后,主要执行的操作是在各个Interceptor中,通过递归执行各个Interceptor,最终返回我们需要的Response对象,这篇笔记主要分析RetryAndFollowUpInterceptor过滤器,主要参考的文章是okhttp源码分析(二)-RetryAndFollowUpInterceptor过滤器

    在上一篇笔记的最后,通过创建RealInterceptorChain对象,执行其proceed()方法,在这个方法中取出RetryAndFollowUpInterceptor对象,执行其中的intercept(Chain)方法,下面是这个方法的源码:

  @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) {
        // The attempt to connect via a route failed. The request will not have been sent.
        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.
        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 {
        followUp = followUpRequest(response, streamAllocation.route());
      } catch (IOException e) {
        streamAllocation.release();
        throw e;
      }

      if (followUp == null) {
        if (!forWebSocket) {
          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;
    }
  }

    可以看到,这个方法的源码是比较长的。首先会获取到RealInterceptorChain中保存的Request对象,这个参数是在创建RealInterceptorChain的时候传递进去的,就是之前构造的Request对象。

    接着将传递进来的Chain对象强制转换为RealInterceptorChain对象,因为在创建Chain对象的时候就直接创建的RealInterceptorChain对象,所以这里是没问题的。

//在AsyncCall中创建的Chain对象:  
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    接着获取Call对象,仍然是之前设置的RealCall对象,从上面的源码也可以看出来,传递Call对象的时候是将this传递进去了。

    接着会获取事件监听,EventListener是在创建RealCall的时候创建的。下面就是获取相关对象的源码:

    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();

    获取完相关对象之后,接下来会创建StreamAllocation对象(这个对象是用来协调Connections,Streams,Calls三者之间的关系的):

    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;

    在创建完相关的对象之后,接着进入到一个死循环while(true),在循环体中,首先判断当前的请求是否已经取消,如果取消了则直接通过抛出异常的方式结束循环:

      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

    如果请求没有取消,则通过realChain.proceed(params)方法执行下一个Interceptor,同时将releaseConnection变量设置为false(这个变量默认为true)。同时会尝试捕获RouteException,如果发现了异常,则会调用recover()方法,下面是这个方法的源码:

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

    // The application layer has forbidden retries.
    if (!client.retryOnConnectionFailure()) return false;

    // We can't send the request body again.
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;

    // This exception is fatal.
    if (!isRecoverable(e, requestSendStarted)) return false;

    // No more routes to attempt.
    if (!streamAllocation.hasMoreRoutes()) return false;

    // For failure recovery, use the same route selector with a new connection.
    return true;
  }

    在recover方法中,进行如下的判断:

  1. 判断如果在OkHttpClient中设置了在连接失败的时候不进行重试,则直接返回false,通过查看OkHttpClient中的源码可以发现,这个参数的默认值是true.
  2. 接着判断如果当前请求已经发送并且当前请求的请求体是UnreapeatableRequestBody(不可重复的请求体),则返回false,但是由于UnrepeatableRequestBody是一个接口,并且并没有找到实现这个接口的类,因此这里应该默认为是不成立.
  3. 接着执行isRecoverable()方法,在这个方法中会分别判断当前的异常是否属于ProtocolException(协议异常),InterruptedIOException(IO中断异常),SSLHandshakeException并且是CertificateException(SSL协议证书异常),SSLPeerUnverifiedException(SSLPeer未验证异常),如果属于这些异常,则返回false.
  4. 接着判断是否还有更多的路由可以试,如果没有,返回false.
  5. 其它情况返回true.

    经过上面的判断,如果返回false,表示不可以进行恢复,此时会抛出异常,然后将releaseConnection设置为false,同时执行continue语句,执行下一次循环。需要注意的是这里还有finally语句块,当然之前还会进行catch IOException的判断,但是和之前的异常捕获是相同的。下面是finally语句块中的内容:

if (releaseConnection) {
         streamAllocation.streamFailed(null);
         streamAllocation.release();
       }

    在finally块中的注释可以知道,如果在执行上面的语句的时候发现了一个未经捕获的异常,则会释放所有资源。调用streamFailed(null)方法。这是因为在finally块中会首先判断releaseConnection变量的值,这个值默认是true,当成功执行try中的语句或者捕获了相关异常的时候会被设置为false,只有抛出一个没有呗捕获的异常的时候才会是true

    成功执行上面的try语句块中的内容之后,接着会判断priorResponse是否为空,如果不为空,则会将之前的请求到的Response附加到当前请求到的Response中。源码如下:

      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

    接下来会创建一个跟进的Request也就是followup,通过followUpRequest()方法为这个变量赋值,源码如下:

  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:
        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:
        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;
        }
        // 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;

        String location = userResponse.header("Location");
        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());
        if (!sameScheme && !client.followSslRedirects()) return null;

        // Most redirects don't include a request body.
        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");
          }
        }

        // 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, 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;
        }

        if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
          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;
    }
  }

    在这个方法中,主要是根据不同的请求状态码设置不同的Request对象,默认是返回null

    获取到followUp实例之后会判断followUp实例是否为null,如果为null,则释放资源,返回当前获取到的Response。这里要和上面的priorResponse结合起来看,如果followUp不是null,则可以认定是请求状态码不是200,出现了某些问题,开始执行后面的代码,在最后会将当次请求到的Response设置给priorResponse,然后开始下一次循环请求。这样也就解释了为什么需要判断priorResponse的情况,同时也解释了为什么会将priorResponsebody设置为null

    在此基础上,我们可以深入到Response.BuilderpriorResponse()方法里面去看:

    public Builder priorResponse(@Nullable Response priorResponse) {
      if (priorResponse != null) checkPriorResponse(priorResponse);
      this.priorResponse = priorResponse;
      return this;
    }

        private void checkPriorResponse(Response response) {
      if (response.body != null) {
        throw new IllegalArgumentException("priorResponse.body != null");
      }
    }

    可以看到,在给当前Response设置priorResponse的时候,如果priorResponsebody不为空则会抛出异常。

    继续分析如果followUp不为空的时候,首先会执行closeQuietly(response.body())关闭当前连接,源码如下:

//Utils类中的closeQuietly方法:
  public static void closeQuietly(Closeable closeable) {
    if (closeable != null) {
      try {
        closeable.close();
      } catch (RuntimeException rethrown) {
        throw rethrown;
      } catch (Exception ignored) {
      }
    }
  }

    这里传递参数的时候使用的是response.body,也就是ResponseBody类的实例,这个类实现了Closeable接口,也就是这里会执行ResponseBodyclose()方法,源码如下:

    private final BufferedSource source;
    private Reader delegate;
    @Override public void close() throws IOException {
      closed = true;
      if (delegate != null) {
        delegate.close();
      } else {
        source.close();
      }
    }

    接着会判断当前请求次数是否大于最大允许的请求次数(20),如果超过了,则释放资源并抛出异常:

      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

    接着判断当前请求体的body是否是UnrepeatableRequestBody,如果是,同样释放资源,抛出异常:

      if (followUp.body() instanceof UnrepeatableRequestBody) {
        streamAllocation.release();
        throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
      }

    接着判断response的连接和followUp的连接是否相同(response是通过执行chain.proceed()获取到的,followUp是通过判断response中的statusCode获取到的)。如果不相同,则会通过followUp.url创建新的StreamAllowcation对象。

    如果相同,但是streamAllocation.codec() != null则会抛出异常,因为在RetryAndFollowUpInterceptor中并没有设置StreamAllocation中的codec属性的值,所以这里如果出现codec != null则说明当前的interceptor有问题,从异常信息也可以推断这一点:

      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?");
      }

    最后将requestpriorResponse分别进行赋值:

      request = followUp;
      priorResponse = response;

    整个执行流程如下:

RetryAndFollowUpInterceptor整体执行流程