“深入交流“系列:Okhttp(二)拦截器的实现

本文深入剖析Okhttp的拦截器机制,详细讲解了RetryAndFollowUpInterceptor如何处理重试和重定向,BridgeInterceptor如何进行网络请求的转换,以及CacheInterceptor如何管理缓存。此外,还探讨了ConnectInterceptor的连接管理和连接池的工作原理。通过这些,读者可以全面了解Okhttp在网络请求中的核心流程。
摘要由CSDN通过智能技术生成

Okhttp拦截器详解

Okhttp拦截器介绍

概念:拦截器是Okhttp中提供的一种强大机制,它可以实现网络监听、请求以及响应重写、请求失败重试等功能。我们先来了解下Okhttp中的系统拦截器:

  • RetryAndFollowUpInterceptor:负责请求失败的时候实现重试重定向功能。
  • BridgeInterceptor:将用户构造的请求转换为向服务器发送的请求,将服务器返回的响应转换为对用户友好的响应。
  • CacheInterceptor:读取缓存、更新缓存。
  • ConnectInterceptor:与服务器建立连接。
  • CallServerInterceptor:从服务器读取响应。

1、拦截器的工作原理

在上一篇文章中我们提到了获取网络请求响应的核心是getResponseWithInterceptorChain()方法,从方法名字也可以看出是通过拦截器链来获取响应,在Okhttp中采用了责任链的设计模式来实现了拦截器链。它可以设置任意数量的Intercepter来对网络请求及其响应做任何中间处理,比如设置缓存,Https证书认证,统一对请求加密/防篡改社会,打印log,过滤请求等等。

责任链模式:在责任链模式中,每一个对象和其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,客户只需要将请求发送到责任链即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。

接下来就通过getResponseWithInterceptorChain()这个方法来具体了解一下:

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    //添加用户自定义的拦截器到拦截器链中,并在系统默认的拦截器之前执行
    interceptors.addAll(client.interceptors());
    //添加重试重定向拦截器
    interceptors.add(retryAndFollowUpInterceptor);
    //添加桥接拦截器,在此拦截器中默认添加许多请求头和解析响应头
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //添加缓存拦截器,根据需要是否从缓存中返回响应
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //添加连接拦截器
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    //发送请求,并获取响应数据
    interceptors.add(new CallServerInterceptor(forWebSocket));
    //创建拦截器链
    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    //通过拦截器执行具体的请求
    return chain.proceed(originalRequest);
  } 

从代码中可以看到首先创建了一个List集合,泛型是Interceptor,也就是拦截器,接着创建了一系列的系统拦截器(一开始介绍的五大拦截器)以及我们自定义的拦截器(client.interceptors()client.networkInterceptors()),并添加到集合中,然后构建了拦截器链RealInterceptorChain,最后通过执行拦截器链的proceed()方法开始了获取服务器响应的整个流程。这个方法也是整个拦截器链的核心,接下来就看一下RealInterceptorChain中的proceed()方法。

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;
    ......

    // Call the next interceptor in the chain.
    // 调用链中的下一个拦截器,index+1代表着下一个拦截器的索引
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    // 取出要调用的拦截器    
    Interceptor interceptor = interceptors.get(index);
    // 调用每个拦截器的intercept方法
    Response response = interceptor.intercept(next);

    ......
    return response;
  } 

proceed()方法的核心就是创建下一个拦截器。首先创建了一个拦截器,并且将index = index+1,然后我们根据index从存放拦截器的集合interceptors中取出当前对应的拦截器,并且调用拦截器中的intercept()方法。这样,当下一个拦截器希望自己的下一级继续处理这个请求的时候,可以调用传入的责任链的proceed()方法。

2、RetryAndFollowUpInterceptor 重试重定向拦截器

RetryAndFollowUpInterceptor:重试重定向拦截器,负责在请求失败的时候重试以及重定向的自动后续请求。但并不是所有的请求失败都可以进行重连。

查看RetryAndFollowUpInterceptor中的intercept方法如下:

@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 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 {
        // 【1】、将请求发给下一个拦截器,在执行的过程中可能会出现异常
      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.
      // 【2】、路由连接失败,请求不会再次发送
      // 在recover方法中会判断是否进行重试,如果不重试抛出异常
      // 在一开始我们提到了并不是所有的失败都可以进行重连,具体哪些请求可以进行重连就在这个recover方法中。
      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.
      // 【3】、尝试与服务器通信失败,请求不会再次发送
      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 {
        // 【4】、在followUpRequest中会判断是否需要重定向,如果需要重定向会返回一个Request用于重定向
      followUp = followUpRequest(response, streamAllocation.route());
    } catch (IOException e) {
      streamAllocation.release();
      throw e;
    }
    // followUp == null 表示不进行重定向,返回response
    if (followUp == null) {
      streamAllocation.release();
      return response;
    }

    closeQuietly(response.body());
     // 【5】、重定向次数最大为20次
    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());
    }
    // 重新创建StreamAllocation实例
    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;
  }
} 

类的介绍

StreamAllocation:维护了服务器连接、并发流和请求(Connection、Streams、Calls)之间的关系,可以为一次请求寻找连接并建立流,从而完成远程通信。在当前的方法中没有用到,会把它们传到之后的拦截器来从服务器中获取请求的响应。

HttpCodec:定义了操作请求和解析响应的方法,实现类为Http1CodecHttp2Codec,分别对应Http1.xHttp2协议。

可以看到其实在RetryAndFollowUpInterceptor中并没有对Request请求做什么特殊的处理,就将请求发送给了下一个拦截器,在拿到后续的拦截器返回的Response之后,RetryAndFollowUpInterceptor主要是根据Response的内容,以此来判断是否进行重试或者重定向的处理。

2.1 重试请求

根据【2】【3】可以得出在请求期间如果发生了RouteException或者IOException会进行判断是否重新发起请求。而这两个异常都是根据recover()来进行判断的,如果recover()返回true,就表示可以进行重试,那么我们就来看一下recover()方法中做了哪些操作。

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

    // 1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试
    //The application layer has forbidden retries.
    if (!client.retryOnConnectionFailure()) return false;

    // 2、如果是RouteException,requestSendStarted这个值为false,无需关心  
    //We can't send the request body again.
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
        return false;

    //todo 3、判断是不是属于重试的异常
    //This exception is fatal.
    if (!isRecoverable(e, requestSendStarted)) return false;

    //todo 4、不存在更多的路由
    //No more routes to attempt.
    if (!streamAllocation.hasMoreRoutes()) return false;

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

1、我们可以在OkhttpClient中配置是否允许进行重试,如果配置了不允许重试,那么请求发生异常后就不会进行重试的操作。

2、如果是RouteExceptionrequestSendStarted这个值为false,无需关心。 如果是IOException,那么requestSendStartedfalse的情况只有在http2io异常的时候出现。那么我们来看第二个条件可以发现UnrepeatableRequestBody是一个接口,这个条件表示如果我们自定义的请求body实现了这个UnrepeatableRequestBody这个接口的时候,就不进行重试请求。

3、判断是不是属于可以重试的异常,主要在isRecoverable()中实现。

private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    
    // 协议异常,ProtocolException异常的时候服务器不会返回内容,不能重试
    // 请求和服务器的响应存在异常,没有按照http的协议来定义,重试也没用
    if (e instanceof ProtocolException) {
        return false;
    }
    // 如果是超时异常,可以进行重试
    // 可能是发生了网络波动导致的Socket连接超时
    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、检查当前有没有可用的路由

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值