okhttp篇4:RetryAndFollowUpInterceptor

news2024/11/27 0:49:40

在上一篇  okhttp篇3:RealCall_yolan6824的博客-CSDN博客 中讲到RealCall无论是在execute还是enqueue方法中,都是通过getResponseWithInterceptorChain方法获取Request对应的Response的。而getResponseWithInterceptorChain这个方法,又是通过RealInterceptorChain这个类串起一系列拦截器顺序执行得到Response的。所以这一篇,先将RealInterceptorChain及第一个拦截器 RetryAndFollowUpInterceptor。

RealInterceptorChain

// RealInterceptorChain
public final class RealInterceptorChain implements Interceptor.Chain {
    public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,
        HttpCodec httpCodec, RealConnection connection, int index, Request request,
         Call call,EventListener eventListener, 
         int connectTimeout, int readTimeout, int writeTimeout) {
      this.interceptors = interceptors;
      this.connection = connection;
      this.streamAllocation = streamAllocation;
      this.httpCodec = httpCodec;
      this.index = index;
      this.request = request;
      this.call = call;
      this.eventListener = eventListener;
      this.connectTimeout = connectTimeout;
      this.readTimeout = readTimeout;
      this.writeTimeout = writeTimeout;
    }
    
    @Override public Response proceed(Request request) throws IOException {
      return proceed(request, streamAllocation, httpCodec, connection);
    }
    
    public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
        RealConnection connection) throws IOException {
      if (index >= interceptors.size()) throw new AssertionError();
    
      calls++;
    
      // If we already have a stream, confirm that the incoming request will use it.
      // httpCodec != null,代表已经有一个stream了
      if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
        throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
            + " must retain the same host and port");
      }
    
      // If we already have a stream, confirm that this is the only call to chain.proceed().
      if (this.httpCodec != null && calls > 1) {
        throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
            + " must call proceed() exactly once");
      }
    
      // Call the next interceptor in the chain.
      // 每一个proceed方法都会新创建一个RealInterceptorChain,对应一个Interceptor
      // 简单的说,RealInterceptorChain就是负责携带下一个Interceptor要用的参数的
      // index指向拦截器列表中当前拦截器的索引。
      // 通过index+1更新调用interceptor.intercept方法
      RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
          connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
          writeTimeout);
      Interceptor interceptor = interceptors.get(index);
      // 将next这个chain传给下一个interceptor
      Response response = interceptor.intercept(next);
    
      // Confirm that the next interceptor made its required call to chain.proceed().
      if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
        throw new IllegalStateException("network interceptor " + interceptor
            + " must call proceed() exactly once");
      }
    
      // Confirm that the intercepted response isn't null.
      if (response == null) {
        throw new NullPointerException("interceptor " + interceptor + " returned null");
      }
    
      if (response.body() == null) {
        throw new IllegalStateException(
            "interceptor " + interceptor + " returned a response with no body");
      }
    
      return response;
    }
}

RealInterceptorChain最重要的就是proceed方法。如上面的注释所说,每调用一次chain.proceed方法,都会新创建一个RealInterceptorChain。

这些RealInterceptorChain共享一开始由RealCall传入的拦截器列表,但是index(对应拦截器的索引)会在每一次proceed方法中更新,实现顺序调用interceptor.proceed方法。

因此,客户端自己实现的拦截器,依然需要调用chain.proceed方法(否则剩下的拦截器不会执行),并且可以通过proceed方法,改变Request, StreamAllocation, HttpCodec , RealConnection这四个参数的值。

RetryAndFollowUpInterceptor

 从RealCall的getResponseWithInterceptorChain,可以看出,如果用户没有自定义拦截器,那么RetryAndFollowUpInterceptor就是实际上第一个执行的拦截器。

intercept

// RetryAndFollowUpInterceptor
@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);

  int followUpCount = 0;
  Response priorResponse = null;
  while (true) {// while循环进行request重试
    if (canceled) {
      streamAllocation.release();
      throw new IOException("Canceled");
    }

    Response response;
    boolean releaseConnection = true;
    try {
        // 通过proceed方法,传入新的参数给后面的拦截器使用。
      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(), false, request)) {// 路由失败
        throw e.getLastConnectException();
      }
      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, 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 = followUpRequest(response);// 根据response构建下一个要请求的Request,一般用于重定向

    if (followUp == null) {
      if (!forWebSocket) {
        streamAllocation.release();
      }
      return response;
    }

    closeQuietly(response.body());

    if (++followUpCount > MAX_FOLLOW_UPS) {// 控制最多只能再次请求20次
      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);
    } 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;
  }
}

followup在翻译中,代表的是跟进的意思,在这里可以理解成一个请求的后续请求,比如说,如果一个请求,因为url改变了,返回了302,那么这时候,可以通过再发送一个新的请求,带上新的url去请求Response。okhttp为我们把这一步给做了。

在上面的intercept方法中,新创建了一个StreamAllocation。这个StreamAllocation是负责协调Connection,Stream,Call三者的关系的。这个后面会详细讲。在这里可以理解成,它可以负责资源的释放。

总结一下intercept方法干的事情:

  1. 如果已经cancel,直接调用streamAllocation.release(释放相关资源)
  2. 调用chain.proceed获取response
    1. 如果获取到RouteException:路由异常 / IOException
      1. client.retryOnConnectionFailure() == false / streamAllocation.hasMoreRoutes() == false / ProtocolException
        1. 不重试
      2. socket超时会重试,SSLHandshakeException,只要不是证书错误,会重试
  3. 根据response获取request(下面的followupRequest方法)
    1. 如果获取到的request为空-->除了认证错误,超时,重定向,其他情况返回的request都是空,这种情况会直接调用streamAllocation.release() 释放资源。
    2. 如果获取到的request不为空,证明需要重试。
    3. 重试的次数最多为20次。

也就是说,路由异常,认证错误,socket超时,重定向,RetryAndFollowUpInterceptor都会按一定的规则,为我们重新构造Request,重新请求。重试的次数最多是20次。

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:// 307,308临时重定向,确保请求方法(get/post)和消息主体不会发生变化
    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:// 永久重定向,method(get/post)有可能会变化
    case HTTP_MOVED_TEMP:
    case HTTP_SEE_OTHER:
      // Does the client allow redirects?
      if (!client.followRedirects()) return null;

      String location = userResponse.header("Location");// 获取response中的新地址
      if (location == null) return null;
      HttpUrl url = userResponse.request().url().resolve(location);// 根据新地址,获取一个新的HttpUrl

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

      return userResponse.request();

    default:
      return null;
  }
}

followRequests方法,主要是根据第一次request返回的response,看看需不需要再次请求,主要分为以下情形:

  1. 407 --> 需要使用代理服务器重新进行身份认证
  2. 401-> 直接进行身份认证
  3. 307/308(method == "get"/"head"), 300/301/302/304 依赖client.followRedirects()
    1. 获取Response中的url,重新构建Request
  4. 408(请求超时) -- 依赖client.retryOnConnectionFailure()
    1. 原request重新请求
  5. 其他情况,都返回空

总结

RetryAndFollowUpInterceptor,跟名字一样,主要负责重试。

跟重试相关联的api(默认都是true,允许重试):

  1. client.followRedirects()
  2. client.retryOnConnectionFailure()

需要重试的情况一般包括:

  1. 路由异常
  2. 身份认证失败
  3. 重定向
  4. 请求超时

最多重试20次。重试的时候,如果发现已经cancel了,取消重试。

下一篇讲下一个拦截器。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/538500.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于PyQt5的图形化界面开发——Windows内存资源监视助手[附带编译exe教程]

基于PyQt5的图形化界面开发——Windows内存资源监视助手[附带编译exe教程] 0. 前言1. 资源信息获取函数——monitor.py2. UI界面——listen.py3. main.py4. 运行效果5. 编译 exe 程序6. 其他PyQt文章 0. 前言 利用 PyQt5 开发一个 windows 的资源监视助手&#xff0c;在使用虚…

【vimsolo】让vim看起来像VSCode:颜色主题和状态栏的配置

文章目录 1. 目的2. 理念&#xff1a; vimsolo3. vimrc: 配置颜色4. vimrc: 配置状态栏5. 拷贝颜色主题和.vimrc: python安装脚本 1. 目的 习惯了 VSCode 默认的配色&#xff1a;黑色主题&#xff0c;蓝色状态栏。偶尔使用 Vim 时想让 vim 伪装的像 VSCode&#xff0c;不考虑花…

Web 测试和 App 测试重点总结

单纯从功能测试的层面上来讲的话&#xff0c;App 测试、Web 测试在流程和功能测试上是没有区别的&#xff0c;但由于系统结构方面存在差异&#xff08;web 项目&#xff0c;b/s 架构&#xff1b;app 项目&#xff0c;c/s 结构&#xff09;在测试中还是有不同的侧重点内容&#…

ZED使用指南(八)Depth Sensing

ZED立体相机再现了人类双目视觉的工作方式。通过比较左眼和右眼看到的两种视图&#xff0c;不仅可以推断深度&#xff0c;还可以推断空间中的3D运动。 ZED立体相机可以捕捉到场景的高分辨率3D视频&#xff0c;通过比较左右图像之间的像素位移可以估计深度和运动。 深度感知 …

CTFHub-ctfhub-Git泄露-Log

CTFHub-ctfhub-Git泄露-Log 当前大量开发人员使用git进行版本控制&#xff0c;对站点自动部署。如果配置不当,可能会将.git文件夹直接部署到线上环境。这就引起了git泄露漏洞。请尝试使用BugScanTeam的GitHack完成本题 1、dirsearch扫描 github上下载dirsearch-master 命令F…

SpringMVC第二阶段:@RequestMapping注解详解

RequestMapping注解详解 RequestMapping是给个方法配置一个访问地址。就比如web学习的Servlet程序&#xff0c;在web.xml中配置了访问地址之后&#xff0c;它们之间就有一个访问映射关系。 1、value属性 value 属性用于配置方法对应的访问地址. /*** RequestMapping 可以配…

JavaScript实现背景图像切换3D动画效果

&#x1f431; 个人主页&#xff1a;不叫猫先生 &#x1f64b;‍♂️ 作者简介&#xff1a;2022年度博客之星前端领域TOP 2&#xff0c;前端领域优质作者、阿里云专家博主&#xff0c;专注于前端各领域技术&#xff0c;共同学习共同进步&#xff0c;一起加油呀&#xff01; &am…

Flask全套知识点从入门到精通,学完可直接做项目

目录 Flask入门 运行方式 URL与函数的映射(动态路由) PostMan的使用 查询参数的获取 上传文件 其它参数 url_for 函数 响应-重定向 响应-响应内容 响应-自定义响应 Flask模板 模板介绍 模板的使用 模板-传参 模板使用url_for函数 过滤器介绍 Jinja模板自带过滤器 流程…

DTFT和DFT有何区别?一文为你讲解清楚

很多人在开始学习数字信号处理的时候&#xff0c;对于各种傅里叶变换特别是离散傅里叶变化的概念及作用完全不清楚&#xff0c;IC修真院在网上整理了关于DTFT、DFT的各知识点。下面就来了解一下关于DTFT和DFT的区别吧。 DTFT&#xff0c; DFT 的区别是含义不同、性质不同、用途…

Elasticsearch集群搭建与相关知识点整理

前言&#xff1a;大家好&#xff0c;我是小威&#xff0c;24届毕业生&#xff0c;在一家满意的公司实习。本篇文章参考网上的课程&#xff0c;介绍Elasticsearch集群的搭建&#xff0c;以及Elasticsearch集群相关知识点整理。 如果文章有什么需要改进的地方还请大佬不吝赐教&am…

C++刷题--选择题4

1, 在&#xff08;&#xff09;情况下适宜采用 inline 定义内联函数 A 函数体含有循环语句 B 函数体含有递归语句 C 函数代码少、频繁调用 D 函数代码多&#xff0c;不常调用 解析 C&#xff0c;以inline修饰的函数叫做内联函数&#xff0c;编译时C编译器会在调用内联函数的地方…

SpringSecurity实现角色权限控制(SpringBoot+SpringSecurity+JWT)

文章目录 一、项目介绍二、SpringSecurity简介SpringSecurity中的几个重要组件&#xff1a;1.SecurityContextHolder&#xff08;class&#xff09;2.SecurityContext&#xff08;Interface&#xff09;3.Authentication&#xff08;Interface&#xff09;4.AuthenticationMana…

c++项目环境搭建(VMware+linux+ubantu+vscode+cmake)

想运行一个c项目&#xff0c;但是环境怎么整呢&#xff1f;b站走起&#xff01;&#xff01;&#xff01; 本文需要的安装包 链接&#xff1a;https://pan.baidu.com/s/1XJbR2F1boQ-CqV8P71UOqw 提取码&#xff1a;swin 一、在虚拟机中安装ubantu 八分钟完成VMware和ubunt…

Git命令大全,涵盖Git全部分类,非常值得收藏!

Git是一个分布式版本控制系统&#xff0c;可以让开发者在不同的平台和环境中协作开发项目。Git有很多命令&#xff0c;可以用来管理项目的状态、历史、分支、合并、冲突等。本文将介绍一些Git常用的命令&#xff0c;并给出示例和分类。 配置命令 配置命令可以用来设置Git的全局…

算法设计与分析:贪心法

目录 第一关&#xff1a;贪心法 任务描述&#xff1a; 相关知识&#xff1a; 贪心法的优缺点&#xff1a; 例题&#xff1a; 解题分析&#xff1a; 程序实现&#xff1a; 关键代码&#xff1a; 编程要求&#xff1a; 测试说明&#xff1a; 第二关&#xff1a;最小生成…

体验了下科大讯飞版ChatGPT,厉害了!

前几天科大讯飞的星火认知大模型发布了&#xff0c;我刚好有朋友在科大讯飞工作&#xff0c;于是就第一时间体验了一波。 一番体验下来确实比我预想的效果要好&#xff0c;没想到国产模型的效果还不错&#xff0c;我试了很多方面&#xff0c;比如通用常识功能、写作功能、学习…

【论文阅读】基于鲁棒强化学习的无人机能量采集可重构智能表面

只做学习记录&#xff0c;侵删原文链接 article{peng2023energy, title{Energy Harvesting Reconfigurable Intelligent Surface for UAV Based on Robust Deep Reinforcement Learning}, author{Peng, Haoran and Wang, Li-Chun}, journal{IEEE Transactions on Wireless Comm…

今日不足——学习目标做了但是没执行

今天复习概率论的时候我发现我复习数值计算方法的时候没有严格按照步骤来&#xff0c;如果按照步骤来我的最小二乘本来可以不用错的。我在复习时候的步骤之间就是抛开书本然后之间进入应用然后遇到不会的回头复习概念虽然缺失能做题目了但是不了解每个知识点的原理和思想&#…

el-drawer 被遮罩层覆盖 显示异常

这是由于元素的一些层级设置不同导致的&#xff0c;所以蒙层被放在了最顶端。解决方法就是加上如下2行代码: <el-drawer title"我是标题" :visible.sync"showDrawer" :direction"ltr" :append-to-body"true":modal-append-to-body&…

【C++ STL】 list 模拟实现

文章目录 &#x1f4cd;前言&#x1f308;STL之list的模拟实现&#x1f388;list_node节点的定义&#x1f388;iterator迭代器&#x1f56f;️构造函数&#x1f56f;️*it&#x1f56f;️->&#x1f56f;️it/it&#x1f56f;️it--/--it&#x1f56f;️! / &#x1f388;l…