Kotlin网络库Fuel的设计之道
使用场景
一个“朴素”的 url 完全可以用一个字符串来表示(例如 "https://www.youzan.com"
),我们可以利用 Kotlin 语言本身的特性为 String
类型添加一个扩展函数 httpGet()
,然后借此发起 http 请求:
1 | "https://www.youzan.com".httpGet() |
但是,对于不是朴素字符串的对象来说,我们可以让其实现一个接口:
1 | interface PathStringConvertible { |
然后,将“计算”过后的 path 通过一个 String
类型提供出来,例如:
1 | enum class HttpsBin(relativePath: String) : Fuel.PathStringConvertible { |
但是,也会存在一种情况,所有的 url 可能会共享一个 base url,或者是其他公用参数,那么还需有一个地方来存储这些通用配置,这个地方的幕后老大就叫 FuelManager
。
String
和 PathStringConvertible
最终也会调用到 FuelManager
。
+----------+
| String |------------->----+
+----------+ | +------+ +-------------+
|--->| Fuel |--->| FuelManager |
+-------------------------+ | +------+ +-------------+
| PathStringConvertible |->-+
+-------------------------+
除了通过 String
或者 PathStringConvertiable
来发起请求,我们还可以直接用一个 Request
,因此 Fuel
还提供了转换 Request
的接口:
1 | interface RequestConvertible { |
综上来看,发起一个 http 请求可以有如下四种方式:
0. 一个字符串
0. PathStringConvertible
变量
0. RequestConvertible
变量
0. 直接使用 Fuel
伴生对象提供的方法
代码实现
对外提供服务的 Fuel
首先 Fuel
作为对外的接口提供方(类似 Facade 模式),通过一个伴生对象(companion object)提供服务(以 get 方法为例):
1 | companion object { |
Fuel 类通过伴生对象提供的 http 方法有 get/post/put/patch/delete/download/upload/head,这些方法最终会路由到 FuleManager
的实例(instance)。
同时,Fule.kt
源文件为 String
和 PathStringConvertible
定义了扩展,以支持这些 http 方法(以 get 方法为例):
1 |
|
幕后老大 FuleManager
FuleManager 利用伴生对象实现了单例模式:
1 | companion object { |
同时利用代理属性实现了单例的懒加载。
readWriteLazy
是一个函数,它的返回值是一个 ReadWriteProperty
,代码比较容易,具体可见 Delegates.kt。
也就是说,当我们第一次访问 FuelManager
时,一个具体的实例会被创建出来,这个实例担负了存储公用配置和发起请求的重任,首先来看它的属性:
1 | var client: Client |
Client
是一个接口,通过它我们可以自定义 http 引擎。
1 | interface Client { |
+---------+ +--------+ +----------+
| Request | ==> | Client | ==> | Response |
+---------+ +--------+ +----------+
|
\|/ +--------------------+
+------------+ | HttpURLConnection |
| HttpClient | --based on-- +--------------------+
+------------+ | HttpsURLConnection |
+--------------------+
Fuel 默认提供的 Http 引擎是 HttpClient
,它是基于 HttpURLConnection 的实现。
basePath
、baseHeaders
和 baseParams
存储了请求的公用配置,我们可以通过 FuleManager.instance
为其赋值:
1 | FuelManager.instance.apply { |
keystore
用于构建 socketFactory
,再加上 hostnameVerifier
,它们用于 https 请求,在 HttpClient
中有用到:
1 | private fun establishConnection(request: Request): URLConnection { |
如果要深入了解 HTTPS 证书,可参考 「HTTPS 精读之 TLS 证书校验」。
FuelManager 在发起请求时会用这些参数构建一个 Request
。
1 | fun request(method: Method, path: String, param: List<Pair<String, Any?>>? = null): Request { |
关于 requestInterceptor
和 responseInterceptor
,原理与 OkHttp 实现的拦截器一致,只不过这里利用了 Kotlin 的高阶函数,代码实现非常简单,具体细节可参考 「Kotlin实战之Fuel的高阶函数」。
跟其他网络库一样,一次完整的请求,必然包含两个实体—— Request
& Response
,先来看 Request
。
请求实体 Request
1 | class Request( |
它支持三种类型的请求:
1 | enum class Type { |
针对每个类型都有对应的任务(task):
1 | //underlying task request |
涉及到上传下载的 DownloadTaskRequest
和 UploadTaskRequest
都继承自 TaskRequest
,它们会处理文件和流相关的东西,关于此可参考 IO 哥写的 一些「流与管道」的小事 以及 OK, IO。
FuelManager
在构造 Request
时用到了一个类——Encoding
:
1 | class Encoding( |
Encoding
也是继承自 Fuel.RequestConvertible
,它完成了对 Request
参数的组装编码,并产生了一个 Request
。
Encoding
组装 query parameter 的方式可以说赏心悦目,贴出来欣赏一下:
1 | private fun queryFromParameters(params: List<Pair<String, Any?>>?): String = params.orEmpty() |
请求返回结果 Response
1 | class Response( |
由 Response
的属性可以看出,它所携带的仍然是一个流(Stream),我们先看 Response
是如何与 Request
串联起来的。
Deserializable.kt
文件为 Request
定了名称为 response
的扩展函数:
1 | private fun <T : Any, U : Deserializable<T>> Request.response( |
扩展函数 response
的参数中,deserializable
负责反序列化操作,success
和 failure
用于处理请求结果。
Fuel 提供了两个 Deserializable
的实现:StringDeserializer
以及 ByteArrayDeserializer
,它们用于反序列化 response 的 stream。
异步请求
Deserializable.kt
为 Request
定义的扩展函数 response
在执行异步操作时用到了一个 AsnycTaskRequest
,其实它本身并不提供异步实现,而是交由一个 ExecutorService
去执行,而这个 ExecutorService
恰由 FuelManager
定义,并在构造 Request
时传入给它。
FuleManager.kt
1 | //background executor |
AsyncTaskRequest
和 UploadTaskRequest
、DownloadTaskRequest
一样,都是继承自 TaskRequest
,只不过它多了两个异步调用的回调:
1 | var successCallback: ((Response) -> Unit)? = null |
请求图例
至此,请求、回复,异步调用,对外接口都了解过了,一个基本的网络库框架已经成型。
+------------------------+
| https://www.youzan.com |
+------------------------+
|
|
\|/
+------+
| Fuel |
+------+
|
|
\|/
+-------------+
| FuelManager |
+-------------+
|
|
\|/
+---------+ +--------+ +----------+
| Request | ===> | Client | ===> | Response |
+---------+ +--------+ +----------+
虽然Fuel 的复杂度不可与 OkHttp 相提并论,但是依赖 Kotlin 语言本身的灵活性,它的代码却比 OkHttp 要简洁的多,特别是关于高阶函数和扩展函数的运用,极大地提升了代码的可读性。