作者: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() 方法源代码
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 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 {
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源码解析系列文章
END
猜你喜欢
-
容易忽视的细节:Log4j 配置导致的零点接口严重超时
-
MySQL主从复制原理剖析与应用实践
-
Hive 和 Spark 分区策略剖析
本文分享自微信公众号 – vivo互联网技术(vivoVMIC)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。