深入浅出 OkHttp 源码解析及应用实践

news2024/12/23 4:52:37

作者:vivo 互联网服务器团队- Tie Qinrui

OkHttp 在 Java 和 Android 世界中被广泛使用,深入学习源代码有助于掌握软件特性和提高编程水平。

本文首先从源代码入手简要分析了一个请求发起过程中的核心代码,接着通过流程图和架构图概括地介绍了OkHttp的整体结构,重点分析了拦截器的责任链模式设计,最后列举了OkHttp拦截器在项目中的实际应用。

一、背景介绍

在生产实践中,常常会遇到这样的场景:需要针对某一类 Http 请求做统一的处理,例如在 Header 里添加请求参数或者修改请求响应等等。这类问题的一种比较优雅的解决方案是使用拦截器来对请求和响应做统一处理。

在 Android 和 Java 世界里 OkHttp 凭借其高效性和易用性被广泛使用。作为一款优秀的开源 Http 请求框架,深入了解它的实现原理,可以学习优秀软件的设计和编码经验,帮助我们更好到地使用它的特性,并且有助于特殊场景下的问题排查。本文尝试从源代码出发探究 OkHttp 的基本原理,并列举了一个简单的例子说明拦截器在我们项目中的实际应用。本文源代码基于 OkHttp 3.10.0。

二、OkHttp 基本原理

2.1 从一个请求示例出发

OkHttp 可以用来发送同步或异步的请求,异步请求与同步请求的主要区别在于异步请求会交由线程池来调度请求的执行。使用 OkHttp 发送一个同步请求的代码相当简洁,示例代码如下:

  • 同步 GET 请求示例

// 1.创建OkHttpClient客户端
OkHttpClient client = new OkHttpClient();
public String getSync(String url) throws IOException {
      OkHttpClient client = new OkHttpClient();
      // 2.创建一个Request对象
      Request request = new Request.Builder()
              .url(url)
              .build();
      // 3.创建一个Call对象并调用execute()方法
      try (Response response = client.newCall(request).execute()) {
          return response.body().string();
      }
  }

其中 execute() 方法是请求发起的入口,RealCall 对象的 execute() 方法的源代码如下:

  • RealCall 的 execute() 方法源代码

@Override public Response execute() throws IOException {
  synchronized (this) { // 同步锁定当前对象,将当前对象标记为“已执行”
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  captureCallStackTrace(); // 捕获调用栈
  eventListener.callStart(this); // 事件监听器记录“调用开始”事件
  try {
    client.dispatcher().executed(this); // 调度器将当前对象放入“运行中”队列
    Response result = getResponseWithInterceptorChain(); // 通过拦截器发起调用并获取响应
    if (result == null) throw new IOException("Canceled");
    return result;
  } catch (IOException e) {
    eventListener.callFailed(this, e); // 异常时记录“调用失败事件”
    throw e;
  } finally {
    client.dispatcher().finished(this); // 将当前对象从“运行中”队列移除
  }
}

execute() 方法首先将当前请求标记为“已执行”,然后会为重试跟踪拦截器添加堆栈追踪信息,接着事件监听器记录“调用开始”事件,调度器将当前对象放入“运行中”队列 ,之后通过拦截器发起调用并获取响应,最后在 finally 块中将当前请求从“运行中”队列移除,异常发生时事件监听器记录“调用失败”事件。其中关键的方法是 getResponseWithInterceptorChain() ,其源代码如下:

Response getResponseWithInterceptorChain() throws IOException {
    // 构建一个全栈的拦截器列表
    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, ……);
 
    return chain.proceed(originalRequest);
  }

该方法中按照特定的顺序创建了一个有序的拦截器列表,之后使用拦截器列表创建拦截器链并发起proceed() 方法调用。在chain.proceed() 方法中会使用递归的方式将列表中的拦截器串联起来依次对请求对象进行处理。拦截器链的实现是 OkHttp 的一个巧妙所在,在后文我们会用一小节专门讨论。在继续往下分析之前,通过以上的代码片段我们已经大致看到了一个请求发起的整体流程。

2.2 OkHttp 核心执行流程

一个 OkHttp 请求的核心执行过程如以下流程图所示:

图 2-1 OkHttp请求执行流程图

图中各部分的含义和作用如下:

  • OkHttpClient:是整个 OkHttp 的核心管理类,从面向对象的抽象表示上来看它代表了客户端本身,是请求的调用工厂,用来发送请求和读取响应。在大多数情况下这个类应该是被共享的,因为每个 Client 对象持有自己的连接池和线程池。重复创建则会造成在空闲池上的资源浪费。Client对象可以通过默认的无参构造方法创建也可以通过 Builder 创建自定义的 Client 对象。Client 持有的线程池和连接池资源在空闲时可以自动释放无需客户端代码手动释放,在特殊情况下也支持手动释放。

  • Request:一个 Request 对象代表了一个 Http 请求。它包含了请求地址 url,请求方法类型 method ,请求头 headers,请求体 body 等属性,该对象具有的属性普遍使用了 final 关键字来修饰,正如该类的说明文档中所述,当这个类的 body 为空或者 body 本身是不可变对象时,这个类是一个不可变对象。

  • Response:一个 Response 对象代表了一个 Http 响应。这个实例对象是一个不可变对象,只有 responseBody 是一个可以一次性使用的值,其他属性都是不可变的。

  • RealCall:一个 RealCall 对象代表了一个准备好执行的请求调用。它只能被执行一次。同时负责了调度和责任链组织的两大重任。

  • Dispatcher:调度器。它决定了异步调用何时被执行,内部使用 ExecutorService 调度执行,支持自定义 Executor。

  • EventListener:事件监听器。抽象类 EventListener 定义了在一个请求生命周期中记录各种事件的方法,通过监听各种事件,可以用来捕获应用程序 HTTP 请求的执行指标。从而监控 HTTP 调用的频率和性能。

  • Interceptor:拦截器。对应了软件设计模式中的拦截器模式,拦截器可用于改变、增强软件的常规处理流程,该模式的核心特征是对软件系统的改变是透明的和自动的。OkHttp 将整个请求的复杂逻辑拆分成多个独立的拦截器实现,通过责任链的设计模式将它们串联到一起,完成发送请求获取响应结果的过程。

2.3 OkHttp 整体架构

通过进一步阅读 OkHttp 源码,可以看到 OkHttp 是一个分层的结构。软件分层是复杂系统设计的常用手段,通过分层可以将复杂问题划分成规模更小的子问题,分而治之。同时分层的设计也有利于功能的封装和复用。OkHttp 的架构可以分为:应用接口层,协议层,连接层,缓存层,I/O层。不同的拦截器为各个层次的处理提供调用入口,拦截器通过责任链模式串联成拦截器链,从而完成一个Http请求的完整处理流程。如下图所示:

图 2-2 OkHttp架构图(图片来自网络)

2.4 OkHttp 拦截器的种类和作用

OkHttp 的核心功能是通过拦截器来实现的,各种拦截器的作用分别为:

  • client.interceptors:由开发者设置的拦截器,会在所有的拦截器处理之前进行最早的拦截处理,可用于添加一些公共参数,如自定义 header、自定义 log 等等。

  • RetryAndFollowUpInterceptor:主要负责进行重试和重定向的处理。

  • BridgeInterceptor:主要负责请求和响应的转换。把用户构造的 request 对象转换成发送到服务器 request对象,并把服务器返回的响应转换为对用户友好的响应。

  • CacheInterceptor:主要负责缓存的相关处理,将 Http 的请求结果放到到缓存中,以便在下次进行相同的请求时,直接从缓存中读取结果,提高响应速度。

  • ConnectInterceptor:主要负责建立连接,建立 TCP 连接或者 TLS 连接。

  • client.networkInterceptors:由开发者设置的拦截器,本质上和第一个拦截器类似,但是由于位置不同,所以用处也不同。

  • CallServerInterceptor:主要负责网络数据的请求和响应,也就是实际的网络I/O操作。将请求头与请求体发送给服务器,以及解析服务器返回的 response。

除了框架提供的拦截器外,OkHttp 支持用户自定义拦截器来对请求做增强处理,自定义拦截器可以分为两类,分别是应用程序拦截器和网络拦截器,他们发挥作用的层次结构如下图:

图 2-3 拦截器(图片来自OkHttp官网)

不同的拦截器有不同的适用场景,他们各自的优缺点如下:

应用程序拦截器

  • 无需担心重定向和重试等中间响应。

  • 总是被调用一次,即使 HTTP 响应是从缓存中提供的。

  • 可以观察到应用程序的原始请求。不关心 OkHttp 注入的标头。

  • 允许短路而不调用 Chain.proceed()方法。

  • 允许重试并多次调用 Chain.proceed()方法。

  • 可以使用 withConnectTimeout、withReadTimeout、withWriteTimeout 调整呼叫超时。

网络拦截器

  • 能够对重定向和重试等中间响应进行操作。

  • 缓存响应不会调用。

  • 可以观察到通过网络传输的原始数据。

  • 可以访问携带请求的链接。

2.5 责任链模式串联拦截器调用

OkHttp 内置了 5 个核心的拦截器用来完成请求生命周期中的关键处理,同时它也支持添加自定义的拦截器来增强和扩展 Http 客户端,这些拦截器通过责任链模式串联起来,使得的请求可以在不同拦截器之间流转和处理。

2.5.1 责任链模式

责任链模式 是一种行为设计模式, 允许将请求沿着处理者链发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下一个处理者。

图 2-4 责任链(图片来自网络)

适用场景 包括:

  • 当程序需要使用不同方式处理不同种类的请求时

  • 当程序必须按顺序执行多个处理者时

  • 当所需要的处理者及其顺序必须在运行时进行改变时

优点

  • 可以控制请求处理的顺序

  • 可对发起操作和执行操作的类进行解耦。

  • 可以在不更改现有代码的情况下在程序中新增处理者。

2.5.2 拦截器的串联

责任链的入口从第一个 RealInterceptorChain 对象的 proceed() 方法调用开始。这个方法的设计非常巧妙,在完整的 proceed() 方法里会做一些更为严谨的校验,去掉这些校验后该方法的核心代码如下:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();
 
    // ……  
    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec, connection, index + 1, request, call, eventListener, connectTimeout, readTimeout, writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);
 
    // ……
    return response;
  }

这段代码可以看成三个步骤:

  1. 索引判断。index 初始值为0,它指示了拦截器对象在列表中的索引顺序,每执行一次 proceed 方法该参数自增1,当索引值大于拦截器列表的索引下标时异常退出。

  2. 创建下一个责任链对象。

  3. 按照索引顺序获取一个拦截器,并调用 intercept() 方法。

  • 责任链串联

单独看这个方法似乎并不能将所有拦截器都串联起来,串联的关键在于 intercept() 方法,intercept() 方法是实现 interceptor 接口时必须要实现的方法,该方法持有下一个责任链 对象 chain,在拦截器的实现类里只需要在 intercept() 方法里的适当地方再次调用 chain.proceed() 方法,程序指令便会重新回到以上代码片段,于是就可以触发对于下一个拦截器的查找和调用了,在这个过程中拦截器对象在列表中的先后顺序非常重要,因为拦截器的调用顺序就是其在列表中的索引顺序。

  • 递归方法

从另一个角度来看,proceed() 方法可以看成是一个递归方法。递归方法的基本定义为“函数的定义中调用函数自身”,虽然 proceed() 方法没有直接调用自身,但是除了最后一个拦截器以外,拦截器链中的其他拦截器都会在适当的位置调用 chain.proceed() 方法,责任链对象和拦截器对象合在一起则组成了一个自己调用自己的逻辑循环。按照笔者个人理解,这也是为什么源代码里 Chain 接口被设计成 Interceptor 接口的内部接口,在理解这段代码的时候要把它们两个接口当成一个整体来看,从这样的角度看的话,这样的接口设计是符合“高内聚”的原则的。

拦截器 interceptor 和责任链 chain 的关系如下图:

图 2-5 Interceptor 和 Chain 的关系图

三、OkHttp 拦截器在项目中的应用

在我们的项目中,有一类请求需要在请求头 Header 中添加认证信息,使用拦截器来实现可以极大地简化代码,提高代码可读性和可维护性。核心代码只需要实现符合业务需要的拦截器如下:

  • 添加请求头的拦截器

public class EncryptInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request originRequest = chain.request();
 
        // 计算认证信息
        String authorization = this.encrypt(originRequest);
         
        // 添加请求头
        Request request = originRequest.newBuilder()
                .addHeader("Authorization", authorization)
                .build();
        // 向责任链后面传递
        return chain.proceed(request);
    }
}

之后在创建 OkHttpClient 客户端的时候,使用 addInterceptor() 方法将我们的拦截器注册成应用程序拦截器,即可实现自动地、无感地向请求头中添加实时的认证信息的功能。

  • 注册应用程序拦截器

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new EncryptInterceptor())
    .build();

四、回顾总结

OkHttp 在 Java 和 Android 世界中被广泛使用,通过使用 OkHttp 拦截器可以解决一类问题——针对一类请求统一修改请求或响应内容。深入了解 OkHttp 的设计和实现不仅可以帮助我们学习优秀开源软件的设计和编码经验,也有利于更好地使用软件特性以及对特殊场景下问题的排查。本文尝试从一个同步 GET 请求的例子开始,首先通过源代码片段简要分析了一个请求发起过程中涉及的核心代码,接着用流程图的形式总结了请求执行过程,然后用架构图展示了OkHttp的分层设计,介绍了各种拦截器的用途、工作层次及优缺点,之后着重分析了拦截器的责任链模式设计——本质是一个递归调用,最后用一个简单的例子介绍了 OkHttp 拦截器在实际生产场景中的应用。

参考:

  1. OkHttp官方文档

  2. OkHttp源码解析系列文章

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

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

相关文章

chatgpt赋能Python-python3_7_6怎么用

Python 3.7.6怎么用 – 简单易上手的编程语言 Python是一种简单易用的编程语言&#xff0c;已经成为了计算机科学领域中最受欢迎的语言之一。Python内置了许多库和工具&#xff0c;可以让用户轻松地完成各种任务。 作为Python的最新版本&#xff0c;Python 3.7.6带来了许多令…

Java Web项目中无效数据怎样处理?

最近在做一个java web项目(自己随便想的)&#xff0c;遇到一个这个的需求&#xff0c;就是从后台数据库根据用户id数据查询用户信息给前端用作个人主页展示吧&#xff01;&#xff0c;但是后台数据库中用户信息中是有密码字段(虽然进行了加密处理)的&#xff0c;对于这个密码数…

一个玩游戏的失足青年,转行做编程到教育的挣扎过程(3/4)

有一个人&#xff0c;从小的心愿是当一名飞行员&#xff0c;终于有一天他当上了飞行员。 但是&#xff0c;他第一次飞行时&#xff0c;飞机就出事了。 好在他能够跳伞。但是&#xff0c;降落却是坏的&#xff0c;打不开...... 突然&#xff0c;他看见了在正下方的地上&#xff…

声音生成——将Autoencoder修改成Variant Autoencoder在mnist训练

文章目录 概述VariantAutoencoder.py文件实现关闭eager execution修改bottlenectk组件修改loss损失函数VariantAutoencoder.py实现的全部代码 train.py文件实现load_mnist模块train模块完整代码执行效果 总结Analysis代码 概述 之前的一篇文章中&#xff0c;介绍了如何实现aut…

if

[rootes3 data]# cat ifaction.sh #!/bin/bash#********************************************************************read -p "请输入身高(m为单位): " HIGH if [[ ! "$HIGH" ~ ^[0-2](\.[0-9]{,2})?$ ]];then echo "输入错误的身高!" exit…

面试字节,简历做了点手脚,第三方背调公司查出来了,被撤销offer!

简历做了手脚被查出来&#xff0c;怎么办&#xff1f; 一位面试字节的程序员哀叹&#xff1a; 运气差&#xff0c;在简历上做了点手脚&#xff0c;被第三方背调查出来了&#xff0c;只能等着被通知撤销offer&#xff01; 有人说&#xff0c;现在背调查的可严格了&#xff0c;字…

Robust 2.0:支持Android R8的升级版热修复框架

2016年&#xff0c;我们对美团Android热更新方案Robust的技术原理做了详细介绍。近几年&#xff0c;Google 推出了新的代码优化混淆工具R8&#xff0c;Android 热修复补丁制作依赖二次构建包和线上包对比&#xff0c;需要对Proguard切换到R8提前进行适配和改造&#xff0c;本文…

鄞州银行:符合中小银行质量提升的数据治理方案

案例简介 在数字化转型的驱动和数据治理“严监管”的推动下&#xff0c;为解决金融机构数据治理体系不健全、数据质量低下等问题&#xff0c;利用数据治理成熟度评估模型进行问题分析定位&#xff0c;重点围绕数据规划、组织机制、标准建设以及数据类平台建设等方面进行数据质…

springboot+jsp网上药品商城销售管理系统

本设计需要实现一套方便药品管理者轻松便捷的处理药品运营工作的药品销售管理系统。设计并实现了特殊药品管理系统。系统选用B/S模式&#xff0c;应用java开发语言&#xff0c; MySQL为后台数据库。系统主要包括主页、个人中心、用户管理、药品类别管理、药品信息管理、系统管理…

Boy,Slowly...

很多朋友问我为啥写的少了。我说很多东西都是常识&#xff0c;老生常谈无数遍了&#xff0c;不想不断重复写了。常识性的东西&#xff0c;不断强调是对的&#xff0c;但是不断重复写&#xff0c;这就不对了。 &#xff08;1&#xff09;朴素 早上看一位朋友发了一条王兴过去老生…

使用Python复制某文件夹下子文件夹名为数据文件夹下的所有以DD开头的文件夹到桌面...

点击上方“Python爬虫与数据挖掘”&#xff0c;进行关注 回复“书籍”即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 楼阁玲珑五云起&#xff0c;其中绰约多仙子。 大家好&#xff0c;我是皮皮。 一、前言 前几天在Python最强王者群【魏哥】问了一个Python自动化办公处理…

macOS Ventura 13.4 (22F66) 正式版发布,ISO、IPSW、PKG 下载

本站下载的 macOS Ventura 软件包&#xff0c;既可以拖拽到 Applications&#xff08;应用程序&#xff09;下直接安装&#xff0c;也可以制作启动 U 盘安装&#xff0c;或者在虚拟机中启动安装。另外也支持在 Windows 和 Linux 中创建可引导介质。 请访问原文链接&#xff1a…

在 Python 中使用 OpenCV 构建 Color Catcher 游戏

介绍 你是否曾经想在 Python 中使用 OpenCV 创建自己的游戏&#xff1f; 今天我们将构建一个名为 Color Catcher 的游戏&#xff0c;该游戏挑战玩家使用手部跟踪机制接住从屏幕顶部掉落的彩球。 设置游戏窗口 构建游戏的第一步是使用 OpenCV 设置游戏窗口。我们将定义窗口大小、…

Swift静态代码检测工程实践

本文字数&#xff1a;22817字 预计阅读时间&#xff1a;58分钟 引言 随着App功能不断增加&#xff0c;工程代码量也随之快速增加&#xff0c;依靠人工CodeReview来保证项目的质量&#xff0c;越来越不现实&#xff0c;这时就有必要借助于自动化的代码审查工具&#xff0c;进行程…

【运维知识进阶篇】集群架构-Nginx七层负载均衡详解

为什么要使用负载均衡 当我们的Web服务器直接面向用户&#xff0c;往往要承载大量并发请求&#xff0c;单台服务器难以负荷&#xff0c;我使用多台Web服务器组成集群&#xff0c;前端使用Nginx负载均衡&#xff0c;将请求分散的打到我们的后端服务器集群中&#xff0c;实现负载…

音频品鉴与歌唱评价——音频内容理解实践

歌唱评价是K歌系统中核心技术之一。近年来&#xff0c;歌唱评价领域也发生着多元化和深度化的变革。本次LiveVideoStackCon 2022 北京站邀请到腾讯音乐天琴实验室高级研究员——江益靓&#xff0c;为大家介绍全民K歌的多维度评价技术和深度歌唱评价技术的实践&#xff0c;以及优…

Linux系统编程学习 NO.3 ——基础指令的学习

* 通配符 通匹配任意字符&#xff08;包括空字符&#xff09;&#xff0c;用于匹配任意长度的字符串。包括空字符。加入你要匹配任何以.txt后缀的文件&#xff0c;只需要在*通配符后加上.txt后缀即可。 样例演示 ls *.后缀名 ls xxx* 找到匹配的字符串man指令(重要) Linux的…

理论力学专题:张量分析

张量方法的引入 自然法则与坐标无关&#xff0c;坐标系的引入方便分析&#xff0c;但也掩盖了物理本质指标符号哑标和自由标 Einstein求和约定&#xff1a;凡在某一项内&#xff0c;重复一次且仅重复一次的指标&#xff0c;表示对该指标在它的取值范围内求和&#xff0c;并称这…

【C++ 入坑指南】(10)函数

文章目录 简介定义实例函数的分文件编写 简介 函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数&#xff0c;即主函数 main() &#xff0c;所有简单的程序都可以定义其他额外的函数。 您可以把代码划分到不同的函数中。如何划分代码到不同的函数中是由您来决定…

分享3个深度学习练手的小案例

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…