- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
作者:vivo 互联网服务器团队- Tie Qinrui 。
OkHttp 在 Java 和 Android 世界中被广泛使用,深入学习源代码有助于掌握软件特性和提高编程水平.
本文首先从源代码入手简要分析了一个请求发起过程中的核心代码,接着通过流程图和架构图概括地介绍了OkHttp的整体结构,重点分析了拦截器的责任链模式设计,最后列举了OkHttp拦截器在项目中的实际应用.
在生产实践中,常常会遇到这样的场景:需要针对某一类 Http 请求做统一的处理,例如在 Header 里添加请求参数或者修改请求响应等等。这类问题的一种比较优雅的解决方案是使用拦截器来对请求和响应做统一处理.
在 Android 和 Java 世界里 OkHttp 凭借其高效性和易用性被广泛使用。作为一款优秀的开源 Http 请求框架,深入了解它的实现原理,可以学习优秀软件的设计和编码经验,帮助我们更好到地使用它的特性,并且有助于特殊场景下的问题排查。本文尝试从源代码出发探究 OkHttp 的基本原理,并列举了一个简单的例子说明拦截器在我们项目中的实际应用。本文源代码基于 OkHttp 3.10.0.
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 的一个巧妙所在,在后文我们会用一小节专门讨论。在继续往下分析之前,通过以上的代码片段我们已经大致看到了一个请求发起的整体流程.
一个 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 将整个请求的复杂逻辑拆分成多个独立的拦截器实现,通过责任链的设计模式将它们串联到一起,完成发送请求获取响应结果的过程.
通过进一步阅读 OkHttp 源码,可以看到 OkHttp 是一个分层的结构。软件分层是复杂系统设计的常用手段,通过分层可以将复杂问题划分成规模更小的子问题,分而治之。同时分层的设计也有利于功能的封装和复用。OkHttp 的架构可以分为:应用接口层,协议层,连接层,缓存层,I/O层。不同的拦截器为各个层次的处理提供调用入口,拦截器通过责任链模式串联成拦截器链,从而完成一个Http请求的完整处理流程。如下图所示:
图 2-2 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 调整呼叫超时.
。
网络拦截器 。
能够对重定向和重试等中间响应进行操作.
缓存响应不会调用.
可以观察到通过网络传输的原始数据.
可以访问携带请求的链接.
OkHttp 内置了 5 个核心的拦截器用来完成请求生命周期中的关键处理,同时它也支持添加自定义的拦截器来增强和扩展 Http 客户端,这些拦截器通过责任链模式串联起来,使得的请求可以在不同拦截器之间流转和处理.
责任链模式 是一种行为设计模式, 允许将请求沿着处理者链发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下一个处理者.
图 2-4 责任链(图片来自网络) 。
适用场景 包括:
当程序需要使用不同方式处理不同种类的请求时 。
当程序必须按顺序执行多个处理者时 。
当所需要的处理者及其顺序必须在运行时进行改变时 。
。
优点 :
可以控制请求处理的顺序 。
可对发起操作和执行操作的类进行解耦.
可以在不更改现有代码的情况下在程序中新增处理者.
责任链的入口从第一个 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 的关系图 。
在我们的项目中,有一类请求需要在请求头 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源码解析系列文章 。
最后此篇关于深入浅出OkHttp源码解析及应用实践的文章就讲到这里了,如果你想了解更多关于深入浅出OkHttp源码解析及应用实践的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我一直在使用 AJAX 从我正在创建的网络服务中解析 JSON 数组时遇到问题。我的前端是一个简单的 ajax 和 jquery 组合,用于显示从我正在创建的网络服务返回的结果。 尽管知道我的数据库查
很难说出这里要问什么。这个问题模棱两可、含糊不清、不完整、过于宽泛或夸夸其谈,无法以目前的形式得到合理的回答。如需帮助澄清此问题以便重新打开,visit the help center . 关闭 1
我在尝试运行 Android 应用程序时遇到问题并收到以下错误 java.lang.NoClassDefFoundError: com.parse.Parse 当我尝试运行该应用时。 最佳答案 在这
有什么办法可以防止etree在解析HTML内容时解析HTML实体吗? html = etree.HTML('&') html.find('.//body').text 这给了我 '&' 但我想
我有一个有点疯狂的例子,但对于那些 JavaScript 函数作用域专家来说,它看起来是一个很好的练习: (function (global) { // our module number one
关闭。此题需要details or clarity 。目前不接受答案。 想要改进这个问题吗?通过 editing this post 添加详细信息并澄清问题. 已关闭 8 年前。 Improve th
我需要编写一个脚本来获取链接并解析链接页面的 HTML 以提取标题和其他一些数据,例如可能是简短的描述,就像您链接到 Facebook 上的内容一样。 当用户向站点添加链接时将调用它,因此在客户端启动
在 VS Code 中本地开发时,包解析为 C:/Users//AppData/Local/Microsoft/TypeScript/3.5/node_modules/@types//index而不是
我在将 json 从 php 解析为 javascript 时遇到问题 这是我的示例代码: //function MethodAjax = function (wsFile, param) {
我在将 json 从 php 解析为 javascript 时遇到问题 这是我的示例代码: //function MethodAjax = function (wsFile, param) {
我被赋予了将一种语言“翻译”成另一种语言的工作。对于使用正则表达式的简单逐行方法来说,源代码过于灵活(复杂)。我在哪里可以了解更多关于词法分析和解析器的信息? 最佳答案 如果你想对这个主题产生“情绪化
您好,我在解析此文本时遇到问题 { { { {[system1];1;1;0.612509325}; {[system2];1;
我正在为 adobe after effects 在 extendscript 中编写一些代码,最终变成了 javascript。 我有一个数组,我想只搜索单词“assemble”并返回整个 jc3_
我有这段代码: $(document).ready(function() { // }); 问题:FB_RequireFeatures block 外部的代码先于其内部的代码执行。因此 who
背景: netcore项目中有些服务是在通过中间件来通信的,比如orleans组件。它里面服务和客户端会指定网关和端口,我们只需要开放客户端给外界,服务端关闭端口。相当于去掉host,这样省掉了些
1.首先贴上我试验成功的代码 复制代码 代码如下: protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
什么是 XML? XML 指可扩展标记语言(eXtensible Markup Language),标准通用标记语言的子集,是一种用于标记电子文件使其具有结构性的标记语言。 你可以通过本站学习 X
【PHP代码】 复制代码 代码如下: $stmt = mssql_init('P__Global_Test', $conn) or die("initialize sto
在SQL查询分析器执行以下代码就可以了。 复制代码代码如下: declare @t varchar(255),@c varchar(255) declare table_cursor curs
前言 最近练习了一些前端算法题,现在做个总结,以下题目都是个人写法,并不是标准答案,如有错误欢迎指出,有对某道题有新的想法的友友也可以在评论区发表想法,互相学习🤭 题目 题目一: 二维数组中的
我是一名优秀的程序员,十分优秀!