Kotlin網(wǎng)絡(luò)庫(kù)Fuel的設(shè)計(jì)之道

使用場(chǎng)景

一個(gè)“樸素”的 url 完全可以用一個(gè)字符串來表示(例如 "https://www.youzan.com"),我們可以利用 Kotlin 語言本身的特性為 String 類型添加一個(gè)擴(kuò)展函數(shù) httpGet()朝卒,然后借此發(fā)起 http 請(qǐng)求:

"https://www.youzan.com".httpGet()

但是涵紊,對(duì)于不是樸素字符串的對(duì)象來說河泳,我們可以讓其實(shí)現(xiàn)一個(gè)接口:

interface PathStringConvertible {
    val path: String
}

然后构诚,將“計(jì)算”過后的 path 通過一個(gè) String 類型提供出來徘层,例如:

enum class HttpsBin(relativePath: String) : Fuel.PathStringConvertible {
    USER_AGENT("user-agent"),
    POST("post"),
    PUT("put"),
    PATCH("patch"),
    DELETE("delete");

    override val path = "https://httpbin.org/$relativePath"
}

但是,也會(huì)存在一種情況吻商,所有的 url 可能會(huì)共享一個(gè) base url掏颊,或者是其他公用參數(shù),那么還需有一個(gè)地方來存儲(chǔ)這些通用配置艾帐,這個(gè)地方的幕后老大就叫 FuelManager乌叶。

StringPathStringConvertible 最終也會(huì)調(diào)用到 FuelManager

+----------+
|  String  |------------->----+
+----------+                  |    +------+    +-------------+
                              |--->| Fuel |--->| FuelManager |
+-------------------------+   |    +------+    +-------------+
|  PathStringConvertible  |->-+
+-------------------------+

除了通過 String 或者 PathStringConvertiable 來發(fā)起請(qǐng)求柒爸,我們還可以直接用一個(gè) Request枉昏,因此 Fuel 還提供了轉(zhuǎn)換 Request 的接口:
除了通過 String 或者 PathStringConvertiable 來發(fā)起請(qǐng)求,我們還可以直接用一個(gè) Request揍鸟,因此 Fuel 還提供了轉(zhuǎn)換 Request 的接口:

interface RequestConvertible {
    val request: Request
}

綜上來看,發(fā)起一個(gè) http 請(qǐng)求可以有如下四種方式:

  1. 一個(gè)字符串
  2. PathStringConvertible 變量
  3. RequestConvertible 變量
  4. 直接使用 Fuel 伴生對(duì)象提供的方法

代碼實(shí)現(xiàn)

對(duì)外提供服務(wù)的 Fuel

首先 Fuel 作為對(duì)外的接口提供方(類似 Facade 模式)句旱,通過一個(gè)伴生對(duì)象(companion object)提供服務(wù)(以 get 方法為例):

companion object {
  @JvmStatic @JvmOverloads
  fun get(path: String, parameters: List<Pair<String, Any?>>? = null): Request =
          request(Method.GET, path, parameters)

  @JvmStatic @JvmOverloads
  fun get(convertible: PathStringConvertible, parameters: List<Pair<String, Any?>>? = null): Request =
          request(Method.GET, convertible, parameters)

  private fun request(method: Method, path: String, parameters: List<Pair<String, Any?>>? = null): Request =
          FuelManager.instance.request(method, path, parameters)

  private fun request(method: Method, convertible: PathStringConvertible, parameters: List<Pair<String, Any?>>? = null): Request =
          request(method, convertible.path, parameters)
}

Fuel 類通過伴生對(duì)象提供的 http 方法有 get/post/put/patch/delete/download/upload/head阳藻,這些方法最終會(huì)路由到 FuleManager 的實(shí)例(instance)。

同時(shí)谈撒,Fule.kt 源文件為 StringPathStringConvertible 定義了擴(kuò)展腥泥,以支持這些 http 方法(以 get 方法為例):

@JvmOverloads
fun String.httpGet(parameters: List<Pair<String, Any?>>? = null): Request = Fuel.get(this, parameters)

@JvmOverloads
fun Fuel.PathStringConvertible.httpGet(parameter: List<Pair<String, Any?>>? = null): Request = Fuel.get(this, parameter)

幕后老大 FuleManager

FuleManager 利用伴生對(duì)象實(shí)現(xiàn)了單例模式:

companion object {
  //manager
  var instance by readWriteLazy { FuelManager() }
}

同時(shí)利用代理屬性實(shí)現(xiàn)了單例的懶加載。

readWriteLazy 是一個(gè)函數(shù)啃匿,它的返回值是一個(gè) ReadWriteProperty蛔外,代碼比較容易蛆楞,具體可見 Delegates.kt

也就是說夹厌,當(dāng)我們第一次訪問 FuelManager 時(shí)豹爹,一個(gè)具體的實(shí)例會(huì)被創(chuàng)建出來,這個(gè)實(shí)例擔(dān)負(fù)了存儲(chǔ)公用配置和發(fā)起請(qǐng)求的重任矛纹,首先來看它的屬性:

var client: Client
var proxy: Proxy?
var basePath: String?

var baseHeaders: Map<String, String>?
var baseParams: List<Pair<String, Any?>>

var keystore: KeyStore?
var socketFactory: SSLSocketFactory

var hostnameVerifier: HostnameVerifier

Client 是一個(gè)接口臂聋,通過它我們可以自定義 http 引擎。

interface Client {
  fun executeRequest(request: Request): Response
}
+---------+     +--------+     +----------+
| Request | ==> | Client | ==> | Response |
+---------+     +--------+     +----------+
                     |
                    \|/                   +--------------------+
              +------------+              | HttpURLConnection  |
              | HttpClient | --based on-- +--------------------+
              +------------+              | HttpsURLConnection |
                                          +--------------------+

Fuel 默認(rèn)提供的 Http 引擎是 HttpClient或南,它是基于 HttpURLConnection 的實(shí)現(xiàn)孩等。
Fuel 默認(rèn)提供的 Http 引擎是 HttpClient,它是基于 HttpURLConnection 的實(shí)現(xiàn)采够。

basePath肄方、baseHeadersbaseParams 存儲(chǔ)了請(qǐng)求的公用配置,我們可以通過 FuleManager.instance 為其賦值:

FuelManager.instance.apply {
  basePath = "http://httpbin.org"
  baseHeaders = mapOf("Device" to "Android")
  baseParams = listOf("key" to "value")
}

keystore 用于構(gòu)建 socketFactory蹬癌,再加上 hostnameVerifier权她,它們用于 https 請(qǐng)求,在 HttpClient 中有用到:

private fun establishConnection(request: Request): URLConnection {
  val urlConnection = if (proxy != null) request.url.openConnection(proxy) else request.url.openConnection()
  return if (request.url.protocol == "https") {
    val conn = urlConnection as HttpsURLConnection
    conn.apply {
      sslSocketFactory = request.socketFactory // socketFactory
      hostnameVerifier = request.hostnameVerifier // hostnameVerifier
    }
  } else {
    urlConnection as HttpURLConnection
  }
}

如果要深入了解 HTTPS 證書冀瓦,可參考 「HTTPS 精讀之 TLS 證書校驗(yàn)」伴奥。

FuelManager 在發(fā)起請(qǐng)求時(shí)會(huì)用這些參數(shù)構(gòu)建一個(gè) Request

fun request(method: Method, path: String, param: List<Pair<String, Any?>>? = null): Request {
  val request = request(Encoding(
        httpMethod = method,
        urlString = path,
        baseUrlString = basePath,
        parameters = if (param == null) baseParams else baseParams + param
  ).request)

  request.client = client
  request.headers += baseHeaders.orEmpty()
  request.socketFactory = socketFactory
  request.hostnameVerifier = hostnameVerifier
  request.executor = createExecutor()
  request.callbackExecutor = callbackExecutor
  request.requestInterceptor = requestInterceptors.foldRight({ r: Request -> r }) { f, acc -> f(acc) }
  request.responseInterceptor = responseInterceptors.foldRight({ _: Request, res: Response -> res }) { f, acc -> f(acc) }
  return request
}

關(guān)于 requestInterceptorresponseInterceptor翼闽,原理與 OkHttp 實(shí)現(xiàn)的攔截器一致拾徙,只不過這里利用了 Kotlin 的高階函數(shù),代碼實(shí)現(xiàn)非常簡(jiǎn)單感局,具體細(xì)節(jié)可參考 「Kotlin實(shí)戰(zhàn)之Fuel的高階函數(shù)」尼啡。

跟其他網(wǎng)絡(luò)庫(kù)一樣,一次完整的請(qǐng)求询微,必然包含兩個(gè)實(shí)體—— Request & Response崖瞭,先來看 Request

請(qǐng)求實(shí)體 Request

class Request(
  val method: Method,
  val path: String,
  val url: URL,
  var type: Type = Type.REQUEST,
  val headers: MutableMap<String, String> = mutableMapOf(),
  val parameters: List<Pair<String, Any?>> = listOf(),
  var name: String = "",
  val names: MutableList<String> = mutableListOf(),
  val mediaTypes: MutableList<String> = mutableListOf(),
  var timeoutInMillisecond: Int = 15000,
  var timeoutReadInMillisecond: Int = timeoutInMillisecond) : Fuel.RequestConvertible

它支持三種類型的請(qǐng)求:

enum class Type {
  REQUEST,
  DOWNLOAD,
  UPLOAD
}

針對(duì)每個(gè)類型都有對(duì)應(yīng)的任務(wù)(task):

//underlying task request
internal val taskRequest: TaskRequest by lazy {
  when (type) {
    Type.DOWNLOAD -> DownloadTaskRequest(this)
    Type.UPLOAD -> UploadTaskRequest(this)
    else -> TaskRequest(this)
  }
}

涉及到上傳下載的 DownloadTaskRequestUploadTaskRequest 都繼承自 TaskRequest撑毛,它們會(huì)處理文件和流相關(guān)的東西书聚,關(guān)于此可參考 IO 哥寫的 一些「流與管道」的小事 以及 OK, IO

FuelManager 在構(gòu)造 Request 時(shí)用到了一個(gè)類——Encoding

class Encoding(
  val httpMethod: Method,
  val urlString: String,
  val requestType: Request.Type = Request.Type.REQUEST,
  val baseUrlString: String? = null,
  val parameters: List<Pair<String, Any?>>? = null) : Fuel.RequestConvertible

Encoding 也是繼承自 Fuel.RequestConvertible藻雌,它完成了對(duì) Request 參數(shù)的組裝編碼雌续,并產(chǎn)生了一個(gè) Request

Encoding 組裝 query parameter 的方式可以說賞心悅目胯杭,貼出來欣賞一下:

private fun queryFromParameters(params: List<Pair<String, Any?>>?): String = params.orEmpty()
  .filterNot { it.second == null }
  .map { (key, value) ->  URLEncoder.encode(key, "UTF-8") to URLEncoder.encode("$value", "UTF-8") }
  .joinToString("&") { (key, value) -> "$key=$value" }

請(qǐng)求返回結(jié)果 Response

class Response(
  val url: URL,
  val statusCode: Int = -1,
  val responseMessage: String = "",
  val headers: Map<String, List<String>> = emptyMap(),
  val contentLength: Long = 0L,
  val dataStream: InputStream = ByteArrayInputStream(ByteArray(0))

Response 的屬性可以看出驯杜,它所攜帶的仍然是一個(gè)流(Stream),我們先看 Response 是如何與 Request 串聯(lián)起來的做个。

Deserializable.kt 文件為 Request 定了名稱為 response 的擴(kuò)展函數(shù):

private fun <T : Any, U : Deserializable<T>> Request.response(
  deserializable: U,
  success: (Request, Response, T) -> Unit,
  failure: (Request, Response, FuelError) -> Unit): Request {

    val asyncRequest = AsyncTaskRequest(taskRequest)

    asyncRequest.successCallback = { response ->
      val deliverable = Result.of { deserializable.deserialize(response) }
      callback {
        deliverable.fold({
          success(this, response, it)
        }, {
          failure(this, response, FuelError(it))
        })
      }
    }

    asyncRequest.failureCallback = { error, response ->
      callback {
        failure(this, response, error)
      }
    }

    submit(asyncRequest)
    return this
}

擴(kuò)展函數(shù) response 的參數(shù)中鸽心,deserializable 負(fù)責(zé)反序列化操作滚局,successfailure 用于處理請(qǐng)求結(jié)果。

Fuel 提供了兩個(gè) Deserializable 的實(shí)現(xiàn):StringDeserializer 以及 ByteArrayDeserializer顽频,它們用于反序列化 response 的 stream藤肢。

異步請(qǐng)求

Deserializable.ktRequest 定義的擴(kuò)展函數(shù) response 在執(zhí)行異步操作時(shí)用到了一個(gè) AsnycTaskRequest,其實(shí)它本身并不提供異步實(shí)現(xiàn)冲九,而是交由一個(gè) ExecutorService 去執(zhí)行谤草,而這個(gè) ExecutorService 恰由 FuelManager 定義,并在構(gòu)造 Request 時(shí)傳入給它莺奸。

FuleManager.kt

//background executor
var executor: ExecutorService by readWriteLazy {
  Executors.newCachedThreadPool { command ->
    Thread(command).also { thread ->
      thread.priority = Thread.NORM_PRIORITY
      thread.isDaemon = true
    }
  }
}

AsyncTaskRequestUploadTaskRequest丑孩、DownloadTaskRequest 一樣,都是繼承自 TaskRequest灭贷,只不過它多了兩個(gè)異步調(diào)用的回調(diào):

var successCallback: ((Response) -> Unit)? = null
var failureCallback: ((FuelError, Response) -> Unit)? = null

請(qǐng)求圖例

至此温学,請(qǐng)求、回復(fù)甚疟,異步調(diào)用仗岖,對(duì)外接口都了解過了,一個(gè)基本的網(wǎng)絡(luò)庫(kù)框架已經(jīng)成型览妖。

         +------------------------+
         | https://www.youzan.com |
         +------------------------+
                     |
                     |
                    \|/
                  +------+
                  | Fuel |
                  +------+
                     |
                     |
                    \|/
              +-------------+
              | FuelManager |
              +-------------+
                     |
                     |
                    \|/
+---------+      +--------+      +----------+
| Request | ===> | Client | ===> | Response |
+---------+      +--------+      +----------+

雖然Fuel 的復(fù)雜度不可與 OkHttp 相提并論轧拄,但是依賴 Kotlin 語言本身的靈活性,它的代碼卻比 OkHttp 要簡(jiǎn)潔的多讽膏,特別是關(guān)于高階函數(shù)和擴(kuò)展函數(shù)的運(yùn)用檩电,極大地提升了代碼的可讀性。

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末俐末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子奄侠,更是在濱河造成了極大的恐慌卓箫,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垄潮,死亡現(xiàn)場(chǎng)離奇詭異烹卒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)弯洗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門甫题,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人涂召,你說我怎么就攤上這事∶舫粒” “怎么了果正?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵炎码,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我秋泳,道長(zhǎng)潦闲,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任迫皱,我火速辦了婚禮歉闰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘卓起。我一直安慰自己和敬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布戏阅。 她就那樣靜靜地躺著昼弟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪奕筐。 梳的紋絲不亂的頭發(fā)上舱痘,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音离赫,去河邊找鬼芭逝。 笑死,一個(gè)胖子當(dāng)著我的面吹牛渊胸,可吹牛的內(nèi)容都是我干的旬盯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蹬刷,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼瓢捉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起办成,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤泡态,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后迂卢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體某弦,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年而克,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了靶壮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡员萍,死狀恐怖腾降,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情碎绎,我是刑警寧澤螃壤,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布抗果,位于F島的核電站,受9級(jí)特大地震影響奸晴,放射性物質(zhì)發(fā)生泄漏冤馏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一寄啼、第九天 我趴在偏房一處隱蔽的房頂上張望逮光。 院中可真熱鬧,春花似錦墩划、人聲如沸涕刚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽副女。三九已至,卻和暖如春蚣旱,著一層夾襖步出監(jiān)牢的瞬間碑幅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工塞绿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留沟涨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓异吻,卻偏偏與公主長(zhǎng)得像裹赴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子诀浪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 前言 人生苦多棋返,快來 Kotlin ,快速學(xué)習(xí)Kotlin雷猪! 什么是Kotlin睛竣? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,201評(píng)論 9 118
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)求摇,斷路器射沟,智...
    卡卡羅2017閱讀 134,651評(píng)論 18 139
  • 《Kotin 編程思想·實(shí)戰(zhàn)》 《Kotlin極簡(jiǎn)教程》正式上架: 點(diǎn)擊這里 > 去京東商城購(gòu)買閱讀 點(diǎn)擊這里 >...
    光劍書架上的書閱讀 2,074評(píng)論 1 4
  • 國(guó)家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說閱讀 10,958評(píng)論 6 13
  • 三年前,由于工作關(guān)系与境,我有機(jī)會(huì)到印度和孟加拉市場(chǎng)做終端銷售管理验夯。雖然市場(chǎng)規(guī)律,管理套路全世界大同小異摔刁,但究其...
    東坡粉可愛多閱讀 594評(píng)論 0 1