拦截器源码分析一

RetryAndFollowUpInterceptor 详解

该拦截器主要是负责失败重连和重定向,我们先了解一下 HTTP 协议中的重定向。

HTTP 协议中的重定向

HTTP 协议提供了一种重定向的功能,它通过由服务器返回特定格式的响应从而触发客户端的重定向。其对应的响应码格式为 3XX,并且会在响应头的 Location 字段中放入新的 URL,这样我们客户端就可以根据该 Location 字段所指定的 URL 重新请求从而得到需要的数据。

源码分析

@Override public Response intercept(Chain chain) throws IOException {
  Request request = chain.request();

  // 创建 StreamAllocation 类,三个参数分别对应:全局的连接池,连接线路Address, 堆栈对象
  streamAllocation = new StreamAllocation(
      client.connectionPool(), createAddress(request.url()), callStackTrace);

  int followUpCount = 0;
  Response priorResponse = null;
  while (true) {
    if (canceled) {
        // 释放资源并抛出异常,结束流程
      streamAllocation.release();
      throw new IOException("Canceled");
    }

    Response response = null;
    boolean releaseConnection = true;
    try {
         //  执行下一个拦截器,即BridgeInterceptor
       // 这里有个很重要的信息,即会将初始化好的连接对象传递给下一个拦截器,
      response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
      releaseConnection = false;
    } catch (RouteException e) {
      // 如果有异常,判断是否要恢复
      if (!recover(e.getLastConnectException(), false, request)) {
        throw e.getLastConnectException();
      }
      releaseConnection = false;
      // 可以恢复请求,继续循环
      continue;
    } catch (IOException e) {
      boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
      if (!recover(e, requestSendStarted, request)) throw e;
      releaseConnection = false;
      continue;
    } finally {
      if (releaseConnection) {
        streamAllocation.streamFailed(null);
        streamAllocation.release();
      }
    }

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

    // 检查是否符合要求
    Request followUp = followUpRequest(response);

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

总共分为如下步骤:

Request 部分:

  1. 根据 url 来构建一个 Address 对象,该对象描述了建立连接的所有配置信息,初始化一个 Socket 连接对象,基于 Okio
  2. Address 对象、 连接池和堆栈对象传入并构建 StreamAllocation 对象,这里只是初始化了一个连接对象,还没真正地去建立连接。
  3. 开启一个死循环,实现不断地重定向。
  4. 如果请求被取消了,则释放资源并抛出异常,循环结束。
  5. 将初始化好的连接对象传递给下一个拦截器(BridgeInterceptor)并执行。

Response 部分:

  1. 如果请求发生异常,判断是否可以恢复请求,不能恢复则抛异常,结果循环。
  2. 如果前一个响应不为空,则结合前一个响应和当前响应。
  3. 根据响应码进行重定向判断,如果重定向后的请求为 null ,即不需要重定向,直接返回响应结果。
  4. 重定向次数 +1 ,并判断是否超过最大次数限制,是则释放资源并抛出异常。
  5. 把当前重定向的请求和响应保存起来,并根据重定向的请求继续循环。

让我们来看一下重定向的方法 followUpRequest 到底做了什么操作:

private Request followUpRequest(Response userResponse) throws IOException {
  if (userResponse == null) throw new IllegalStateException();
  Connection connection = streamAllocation.connection();
  Route route = connection != null
      ? connection.route()
      : null;
  int responseCode = userResponse.code();

  final String method = userResponse.request().method();
  switch (responseCode) {
    case HTTP_PROXY_AUTH:        //407,代理身份认证
      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:    // 401 ,身份认证
      return client.authenticator().authenticate(route, userResponse);

    case HTTP_PERM_REDIRECT:    // 308
    case HTTP_TEMP_REDIRECT:    //307
      // 只对 GET 和 HEAD 请求做重定向处理
      if (!method.equals("GET") && !method.equals("HEAD")) {
        return null;
      }
      // fall-through
    case HTTP_MULT_CHOICE:    //300
    case HTTP_MOVED_PERM:        //301
    case HTTP_MOVED_TEMP:        //302
    case HTTP_SEE_OTHER:        //303
      // Does the client allow redirects?
      // 如果客户端关闭了重定向,则直接返回 null
      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;

      if (!sameScheme && !client.followSslRedirects()) return null;

      Request.Builder requestBuilder = userResponse.request().newBuilder();
      // 处理重定向使用的 method
      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");
      }
      // 重新构建新的 request
      return requestBuilder.url(url).build();

    case HTTP_CLIENT_TIMEOUT:        // 408 ,需要重新发送一次相同的请求
      if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
        return null;
      }

      return userResponse.request();

    default:
      return null;
  }
}

主要是根据响应码和响应头,查看是否需要重定向,并重新设置请求。

BridgeInterceptor 详解

BridgeInterceptor 的名字取的非常形象,它就像一座桥梁,连接了用户与服务器。在用户向服务器发送请求时,它会把用户所构建的请求转换为向服务器请求的真正的 Request,而在服务器返回了响应后,它又会将服务器所返回的响应转换为用户所能够使用的 Response

@Override public Response intercept(Chain chain) throws IOException {
  Request userRequest = chain.request();
  Request.Builder requestBuilder = userRequest.newBuilder();

  RequestBody body = userRequest.body();
  // 将一些属性添加到请求头里
  if (body != null) {
    MediaType contentType = body.contentType();
    if (contentType != null) {
      requestBuilder.header("Content-Type", contentType.toString());
    }

    long contentLength = body.contentLength();
    if (contentLength != -1) {
      requestBuilder.header("Content-Length", Long.toString(contentLength));
      requestBuilder.removeHeader("Transfer-Encoding");
    } else {
      requestBuilder.header("Transfer-Encoding", "chunked");
      requestBuilder.removeHeader("Content-Length");
    }
  }

  if (userRequest.header("Host") == null) {
    requestBuilder.header("Host", hostHeader(userRequest.url(), false));
  }

  if (userRequest.header("Connection") == null) {
    requestBuilder.header("Connection", "Keep-Alive");
  }

  boolean transparentGzip = false;
  // 若未设置Accept-Encoding,自动设置gzip
  if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
    transparentGzip = true;
    requestBuilder.header("Accept-Encoding", "gzip");
  }

  // 获取 cookieJar 返回的 cookie 列表
  List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
  if (!cookies.isEmpty()) {
      // 根据 Okhpptclitent时候配置的 cookieJar 添加 Cookie 头信息
    requestBuilder.header("Cookie", cookieHeader(cookies));
  }

  if (userRequest.header("User-Agent") == null) {
    requestBuilder.header("User-Agent", Version.userAgent());
  }

  // 上述的处理,都是请求头的处理
  // 把处理了请求头的请求传给下一个拦截器做处理
  Response networkResponse = chain.proceed(requestBuilder.build());

  // 保存响应的 cookie 信息
  HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

  // 下面是响应头信息处理
  Response.Builder responseBuilder = networkResponse.newBuilder()
      .request(userRequest);

  // 如果服务器返回的响应content是以gzip压缩过的,则会交给Okio先进行解压缩,
  // 移除响应中的header Content-Encoding和Content-Length,构造新的响应返回。
  if (transparentGzip
      && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
      && HttpHeaders.hasBody(networkResponse)) {
    GzipSource responseBody = new GzipSource(networkResponse.body().source());
    Headers strippedHeaders = networkResponse.headers().newBuilder()
        .removeAll("Content-Encoding")
        .removeAll("Content-Length")
        .build();
    responseBuilder.headers(strippedHeaders);
    // 处理完成后,重新生成一个 response
    responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
  }
  // 返回新的 response
  return responseBuilder.build();
}

总共分为如下步骤:

Request 部分:

  1. 给请求头添加一些 head 信息,包括 Content-Type、 Content-Length、 Transfer-Encoding、 Host、 Connection、 Accept-Encoding、 User-Agent 等。
  2. 若调用者未设置 Accept-Encoding ,则它会默认设置 gzip ,进行 gzip 压缩。
  3. 如果有 Cookie ,则添加 Cookie 头信息。
  4. 将包装过的请求对象传递给下一个拦截器(CacheInterceptor)并执行。

Response 部分:

  1. 将返回的响应里面的 Cookie 保存起来。
  2. 如果服务器返回的响应 content 是以 gzip 压缩过的,则会交给 Okio 先进行解压缩并移除响应中的 header Content-Encoding 和 Content-Length ,构造新的响应返回。
  3. 否则,直接返回 response 。

其中 CookieJar 来自 OkHttpClient , 它是 OKHttp 的 Cookie 管理类,负责 Cookie 的存取。由于 CookieJar 是一个接口类,我们在构建 OkHttpClient 时,可以通过 builder 输入 CookieJar 的实现类。下面我们看一下 CookieJar 的源码:

public interface CookieJar {
  // 默认没有 Cookie 的实现类
  CookieJar NO_COOKIES = new CookieJar() {
    @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
    }

    @Override public List<Cookie> loadForRequest(HttpUrl url) {
      return Collections.emptyList();
    }
  };


  void saveFromResponse(HttpUrl url, List<Cookie> cookies);


  List<Cookie> loadForRequest(HttpUrl url);
}

由源码可知,OkHttp 默认不处理 Cookie ,所以如果想对 Cookie 进行管理,需要自己实现该实现,在对应的接口方法里面对 Cookie 进行存取操作。需要注意的是,loadForRequest() 方法的返回值不能为 null