深入浅出 OkHttp 源码解析及应用实践
作者: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;
}
这段代码可以看成三个步骤:
索引判断。index 初始值为0,它指示了拦截器对象在列表中的索引顺序,每执行一次 proceed 方法该参数自增1,当索引值大于拦截器列表的索引下标时异常退出。
创建下一个责任链对象。
按照索引顺序获取一个拦截器,并调用 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 拦截器在实际生产场景中的应用。
参考:
深入浅出 OkHttp 源码解析及应用实践的更多相关文章
- OKHttp源码解析
http://frodoking.github.io/2015/03/12/android-okhttp/ Android为我们提供了两种HTTP交互的方式:HttpURLConnection 和 A ...
- 深入浅出ReentrantLock源码解析
ReentrantLock不但是可重入锁,而且还是公平或非公平锁,在工作中会经常使用到,将自己对这两种锁的理解记录下来,希望对大家有帮助. 前提条件 在理解ReentrantLock时需要具备一些基本 ...
- 深入浅出ReentrantReadWriteLock源码解析
读写锁实现逻辑相对比较复杂,但是却是一个经常使用到的功能,希望将我对ReentrantReadWriteLock的源码的理解记录下来,可以对大家有帮助 前提条件 在理解ReentrantReadWri ...
- 深入浅出Semaphore源码解析
Semaphore通过permits的值来限制线程访问临界资源的总数,属于有限制次数的共享锁,不支持重入. 前提条件 在理解Semaphore时需要具备一些基本的知识: 理解AQS的实现原理 之前有写 ...
- 【转载】okhttp源码解析
转自:http://www.open-open.com/lib/view/open1472216742720.html https://blog.piasy.com/2016/07/11/Unders ...
- Feign源码解析系列-最佳实践
前几篇准备写完feign的源码,这篇直接给出Feign的最佳实践,考虑到目前网上还没有一个比较好的实践解释,对于新使用spring cloud的同学会对微服务之间的依赖产生一些迷惑,也会走一些弯路.这 ...
- 深入浅出AQS源码解析
最近一直在研究AQS的源码,希望可以更深刻的理解AQS的实现原理.虽然网上有很多关于AQS的源码分析,但是看完以后感觉还是一知半解.于是,我将自己的整个理解过程记录下来了,希望对大家有所帮助. 基本原 ...
- Retrofit源码解析(下)
接着上一章继续分析上一章主要简单说了一下基本使用和注解,这一章,我们主要看源码,废话不多说了,直接上.先上一张图 从网络上拿来的 前面一章说了一下Retrofit的简单使用https://www.cn ...
- Retrofit源码解析(上)
简介Retrofit是Square公司开发的一款针对Android网络请求的框架,官网地址http://square.github.io/retrofit/ ,在官网上有这样的一句话介绍retrofi ...
- Scala 深入浅出实战经典 第65讲:Scala中隐式转换内幕揭秘、最佳实践及其在Spark中的应用源码解析
王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...
随机推荐
- gRPC之.Net6中的初步使用说明
1.介绍 GRPC是一个高性能.通用的开源远程过程调用(RPC)框架,基于底层HTTP/2协议标准和协议层Protobuf序列化协议开发,支持众多的开发语言,由Google开源. gRPC也是基于以下 ...
- 4.0 SDK Workshop 纪实:一起体验多人、多屏幕共享新功能
在本月初,声网发布了 RTC Native SDK 4.0 版本.该版本提供了更高的开发灵活度,可明显提升实时场景开发效率,并让第三方插件开发更容易.上周六(8月20日),我们组织了一场小型的线下 W ...
- MATLAB信号处理常用函数(转载)
https://shimo.im/docs/YyRXY8cQdqY8RJvc/ <MATLAB信号处理工具箱>,可复制链接后用石墨文档 App 或小程序打开 嗯这个肯定是随便看看,有个印象 ...
- salesforce零基础学习(一百一十六)workflow -> flow浅谈
本篇参考: https://help.salesforce.com/s/articleView?id=sf.migrate_to_flow_tool_considerations_workflow.h ...
- VUE3.x之Proxy 我们为什么要使用Proxy
Object.defineProperty 劫持数据 只是对对象的属性进行劫持 无法监听新增属性和删除属性 需要使用 vue.set, vue.delete 深层对象的劫持需要一次性递归 劫持数组时需 ...
- VUE-生命周期/请求数据
使用方法 --- 4个before,4个ed,创造,装载,更新,销毁 初始化阶段 beforeCreate(){} // 准备怀孕 created(){} // 怀上了 *************** ...
- 两条命令搞定 ChatGPT API 的调用问题
自2022年11月30日 OpenAI 发布 ChatGPT 以来,虽然时有唱衰的声音出现,但在OpenAI不断推陈出新,陆续发布了OpenAPI.GPT-4.ChatGPT Plugins之后,似乎 ...
- mesql输入中文报错
错误提示:ERROR 1366 (HY000): Incorrect string value: '\xE6\x9D\x8E\xE5\x8B\x87' for column 'Sname' at ro ...
- [Windows]CMD命令入门教程 与 Windows常见维护问题
本博文最早是记录在本地电脑的,由于清理电脑的缘故,顺便将这篇笔记转移到公共博客,以便日后查阅和快速上手使用. 开门见山,步入正题,以下是Windows系统的常用CMD命令. ----2018-03-2 ...
- Java设计模式 —— 装饰模式
12 装饰模式 12.1 装饰模式概述 Decorator Pattern: 动态地给一个对象增加一些额外的职责.提供一种比使用子类更加灵活的方案来扩展功能. 装饰模式是一种用于替代继承的技术,通过一 ...