作者:vivo 互联网服务器团队 – Tie Qinrui
本文首先从源代码入手简要分析了一个请求发起过程中的核心代码,接着通过流程图和架构图概括地介绍了 OkHttp 的整体结构,重点分析了拦截器的责任链模式设计,最后列举了 OkHttp 拦截器在项目中的实际应用。
在 Android 和 Java 世界里 OkHttp 凭借其高效性和易用性被广泛使用。作为一款优秀的开源 Http 请求框架,深入了解它的实现原理,可以学习优秀软件的设计和编码经验,帮助我们更好到地使用它的特性,并且有助于特殊场景下的问题排查。本文尝试从源代码出发探究 OkHttp 的基本原理,并列举了一个简单的例子说明拦截器在我们项目中的实际应用。本文源代码基于 OkHttp 3.10.0。
2.1 从一个请求示例出发
OkHttp 可以用来发送同步或异步的请求,异步请求与同步请求的主要区别在于异步请求会交由线程池来调度请求的执行。使用 OkHttp 发送一个同步请求的代码相当简洁,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@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 () 方法是请求发起的入口,RealCall 对象的 execute () 方法的源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@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 () 其源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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 请求执行流程图
图中各部分的含义和作用如下:
2.3 OkHttp 整体架构
通过进一步阅读 OkHttp 源码,可以看到 OkHttp 是一个分层的结构。软件分层是复杂系统设计的常用手段,通过分层可以将复杂问题划分成规模更小的子问题,分而治之。同时分层的设计也有利于功能的封装和复用。OkHttp 的架构可以分为:应用接口层,协议层,连接层,缓存层,I/O 层。不同的拦截器为各个层次的处理提供调用入口,拦截器通过责任链模式串联成拦截器链,从而完成一个 Http 请求的完整处理流程。如下图所示:
图 2-2 OkHttp 架构图(图片来自网络)
2.4 OkHttp 拦截器的种类和作用
OkHttp 的核心功能是通过拦截器来实现的,各种拦截器的作用分别为:
除了框架提供的拦截器外,OkHttp 支持用户自定义拦截器来对请求做增强处理,自定义拦截器可以分为两类,分别是应用程序拦截器和网络拦截器,他们发挥作用的层次结构如下图:
图 2-3 拦截器(图片来自 OkHttp 官网)
不同的拦截器有不同的适用场景,他们各自的优缺点如下:
应用程序拦截器
网络拦截器
2.5 责任链模式串联拦截器调用
OkHttp 内置了 5 个核心的拦截器用来完成请求生命周期中的关键处理,同时它也支持添加自定义的拦截器来增强和扩展 Http 客户端,这些拦截器通过责任链模式串联起来,使得的请求可以在不同拦截器之间流转和处理。
2.5.1 责任链模式
责任链模式 是一种行为设计模式, 允许将请求沿着处理者链发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下一个处理者。
图 2-4 责任链(图片来自网络)
优点:
2.5.2 拦截器的串联
责任链的入口从第一个 RealInterceptorChain 对象的 proceed () 方法调用开始。这个方法的设计非常巧妙,在完整的 proceed () 方法里会做一些更为严谨的校验,去掉这些校验后该方法的核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
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; } |
这段代码可以看成三个步骤:
单独看这个方法似乎并不能将所有拦截器都串联起来,串联的关键在于 intercept () 方法,intercept () 方法是实现 interceptor 接口时必须要实现的方法,该方法持有下一个责任链 对象 chain,在拦截器的实现类里只需要在 intercept () 方法里的适当地方再次调用 chain.proceed () 方法,程序指令便会重新回到以上代码片段,于是就可以触发对于下一个拦截器的查找和调用了,在这个过程中拦截器对象在列表中的先后顺序非常重要,因为拦截器的调用顺序就是其在列表中的索引顺序。
递归方法
从另一个角度来看,proceed () 方法可以看成是一个递归方法。递归方法的基本定义为 “函数的定义中调用函数自身”,虽然 proceed () 方法没有直接调用自身,但是除了最后一个拦截器以外,拦截器链中的其他拦截器都会在适当的位置调用 chain.proceed () 方法,责任链对象和拦截器对象合在一起则组成了一个自己调用自己的逻辑循环。按照笔者个人理解,这也是为什么源代码里 Chain 接口被设计成 Interceptor 接口的内部接口,在理解这段代码的时候要把它们两个接口当成一个整体来看,从这样的角度看的话,这样的接口设计是符合 “高内聚” 的原则的。
拦截器 interceptor 和责任链 chain 的关系如下图:
图 2-5 Interceptor 和 Chain 的关系图
在我们的项目中,有一类请求需要在请求头 Header 中添加认证信息,使用拦截器来实现可以极大地简化代码,提高代码可读性和可维护性。核心代码只需要实现符合业务需要的拦截器如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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 () 方法将我们的拦截器注册成应用程序拦截器,即可实现自动地、无感地向请求头中添加实时的认证信息的功能。
1 2 3 |
OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new EncryptInterceptor()) .build(); |
OkHttp 在 Java 和 Android 世界中被广泛使用,通过使用 OkHttp 拦截器可以解决一类问题 —— 针对一类请求统一修改请求或响应内容。深入了解 OkHttp 的设计和实现不仅可以帮助我们学习优秀开源软件的设计和编码经验,也有利于更好地使用软件特性以及对特殊场景下问题的排查。本文尝试从一个同步 GET 请求的例子开始,首先通过源代码片段简要分析了一个请求发起过程中涉及的核心代码,接着用流程图的形式总结了请求执行过程,然后用架构图展示了 OkHttp 的分层设计,介绍了各种拦截器的用途、工作层次及优缺点,之后着重分析了拦截器的责任链模式设计 —— 本质是一个递归调用,最后用一个简单的例子介绍了 OkHttp 拦截器在实际生产场景中的应用。
from:https://my.oschina.net/vivotech/blog/8817305