基于 Kotlin + OkHttp 實現(xiàn)易用且功能強大的網(wǎng)絡(luò)框架(一)

okhttp-extension 是針對 okhttp 3 增強的網(wǎng)絡(luò)框架咪笑。使用 Kotlin 特性編寫祠丝,提供便捷的 DSL 方式創(chuàng)建網(wǎng)絡(luò)請求橘茉,支持協(xié)程、響應(yīng)式編程等等展鸡。

其 core 模塊只依賴 OkHttp屿衅,不會引入第三方庫。

okhttp-extension 可以整合 Retrofit莹弊、Feign 框架涤久,還提供了很多常用的攔截器。 另外忍弛,okhttp-extension 也給開發(fā)者提供一種新的選擇响迂。

github地址:https://github.com/fengzhizi715/okhttp-extension

Features:

  • 支持 DSL 創(chuàng)建 HTTP GET/POST/PUT/HEAD/DELETE/PATCH requests.
  • 支持 Kotlin 協(xié)程
  • 支持響應(yīng)式(RxJava、Spring Reactor)
  • 支持函數(shù)式
  • 支持熔斷器(Resilience4j)
  • 支持異步請求的取消
  • 支持 Request细疚、Response 的攔截器
  • 提供常用的攔截器
  • 支持自定義線程池
  • 支持整合 Retrofit栓拜、Feign 框架
  • 支持 Websocket 的實現(xiàn)、自動重連等
  • core 模塊只依賴 OkHttp惠昔,不依賴其他第三方庫
okhttp-extension.png

一. General

1.1 Basic

無需任何配置(零配置)即可直接使用幕与,僅限于 Get 請求。

    "https://baidu.com".httpGet().use {
        println(it)
    }

或者需要依賴協(xié)程镇防,也僅限于 Get 請求啦鸣。

   "https://baidu.com".asyncGet()
       .await()
       .use {
           println(it)
       }

1.2 Config

配置 OkHttp 相關(guān)的參數(shù)以及攔截器,例如:

const val DEFAULT_CONN_TIMEOUT = 30

val loggingInterceptor by lazy {
    LogManager.logProxy(object : LogProxy {  // 必須要實現(xiàn) LogProxy 来氧,否則無法打印網(wǎng)絡(luò)請求的 request 诫给、response
        override fun e(tag: String, msg: String) {
        }

        override fun w(tag: String, msg: String) {
        }

        override fun i(tag: String, msg: String) {
            println("$tag:$msg")
        }

        override fun d(tag: String, msg: String) {
            println("$tag:$msg")
        }
    })

    LoggingInterceptor.Builder()
        .loggable(true) // TODO: 發(fā)布到生產(chǎn)環(huán)境需要改成false
        .request()
        .requestTag("Request")
        .response()
        .responseTag("Response")
//        .hideVerticalLine()// 隱藏豎線邊框
        .build()
}

val httpClient: HttpClient by lazy {
    HttpClientBuilder()
        .baseUrl("http://localhost:8080")
        .allTimeouts(DEFAULT_CONN_TIMEOUT.toLong(), TimeUnit.SECONDS)
        .addInterceptor(loggingInterceptor)
        .addInterceptor(CurlLoggingInterceptor())
        .serializer(GsonSerializer())
        .jsonConverter(GlobalRequestJSONConverter::class)
        .build()
}

配置完之后,就可以直接使用 httpClient

    httpClient.get{

        url {
            url = "/response-headers-queries"

            "param1" to "value1"
            "param2" to "value2"
        }

        header {
            "key1" to "value1"
            "key2" to "value2"
        }
    }.use {
        println(it)
    }

這里的 url 需要和 baseUrl 組成完整的 url啦扬。比如:http://localhost:8080/response-headers-queries
當然中狂,也可以使用 customUrl 替代 baseUrl + url 作為完整的 url

1.3 AOP

針對所有 request、response 做一些類似 AOP 的行為扑毡。

需要在構(gòu)造 httpClient 時胃榕,調(diào)用 addRequestProcessor()、addResponseProcessor() 方法瞄摊,例如:

val httpClientWithAOP by lazy {
    HttpClientBuilder()
        .baseUrl("http://localhost:8080")
        .allTimeouts(DEFAULT_CONN_TIMEOUT.toLong(), TimeUnit.SECONDS)
        .addInterceptor(loggingInterceptor)
        .serializer(GsonSerializer())
        .jsonConverter(GlobalRequestJSONConverter::class)
        .addRequestProcessor { _, builder ->
            println("request start")
            builder
        }
        .addResponseProcessor {
            println("response start")
        }
        .build()
}

這樣在進行 request勋又、response 時苦掘,會分別打印"request start"和"response start"。

因為在創(chuàng)建 request 之前楔壤,會處理所有的 RequestProcessor鹤啡;在響應(yīng) response 之前,也會用內(nèi)部的 ResponseProcessingInterceptor 攔截器來處理 ResponseProcessor蹲嚣。

RequestProcessor递瑰、ResponseProcessor 分別可以認為是 request、response 的攔截器隙畜。

// a request interceptor
typealias RequestProcessor = (HttpClient, Request.Builder) -> Request.Builder

// a response interceptor
typealias ResponseProcessor = (Response) -> Unit

我們可以多次調(diào)用 addRequestProcessor() 泣矛、addResponseProcessor() 方法。

二. DSL

DSL 是okhttp-extension框架的特色禾蚕。包含使用 DSL 創(chuàng)建各種 HTTP Request 和使用 DSL 結(jié)合聲明式編程您朽。

2.1 HTTP Request

使用 DSL 支持創(chuàng)建GET/POST/PUT/HEAD/DELETE/PATCH

2.1.1 get

最基本的 get 用法

    httpClient.get{

        url {
            url = "/response-headers-queries"

            "param1" to "value1"
            "param2" to "value2"
        }

        header {
            "key1" to "value1"
            "key2" to "value2"
        }
    }.use {
        println(it)
    }

這里的 url 需要和 baseUrl 組成完整的 url。比如:http://localhost:8080/response-headers-queries
當然换淆,也可以使用 customUrl 替代 baseUrl + url 作為完整的 url

2.1.2 post

基本的 post 請求如下:

    httpClient.post{

        url {
            url = "/response-body"
        }

        header {
            "key1" to "value1"
            "key2" to "value2"
        }

        body {
            form {
                "form1" to "value1"
                "form2" to "value2"
            }
        }
    }.use {
        println(it)
    }

支持 request body 為 json 字符串

    httpClient.post{

        url {
            url = "/response-body"
        }

        body("application/json") {
            json {
                "key1" to "value1"
                "key2" to "value2"
                "key3" to "value3"
            }
        }
    }.use {
        println(it)
    }

支持單個/多個文件的上傳

    val file = File("/Users/tony/Downloads/xxx.png")

    httpClient.post{

        url {
            url = "/upload"
        }

        multipartBody {
            +part("file", file.name) {
                file(file)
            }
        }
    }.use {
        println(it)
    }

更多 post 相關(guān)的方法哗总,歡迎使用者自行探索。

2.1.3 put

    httpClient.put{

        url {
            url = "/response-body"
        }

        header {
            "key1" to "value1"
            "key2" to "value2"
        }

        body("application/json") {
            string("content")
        }
    }.use {
        println(it)
    }

2.1.4 delete

    httpClient.delete{

        url {
            url = "/users/tony"
        }
    }.use {
        println(it)
    }

2.1.5 head

    httpClient.head{

        url {
            url = "/response-headers"
        }

        header {
            "key1" to "value1"
            "key2" to "value2"
            "key3" to "value3"
        }
    }.use {
        println(it)
    }

2.1.6 patch

    httpClient.patch{

        url {
            url = "/response-body"
        }

        header {
            "key1" to "value1"
            "key2" to "value2"
        }

        body("application/json") {
            string("content")
        }
    }.use {
        println(it)
    }

2.2 Declarative

像使用 Retrofit倍试、Feign 一樣讯屈,在配置完 httpClient 之后,需要定義一個 ApiService 它用于聲明所調(diào)用的全部接口县习。ApiService 所包含的方法也是基于 DSL 的涮母。例如:

class ApiService(client: HttpClient) : AbstractHttpService(client) {

    fun testGet(name: String) = get<Response> {
        url = "/sayHi/$name"
    }

    fun testGetWithPath(path: Map<String, String>) = get<Response> {
        url = "/sayHi/{name}"
        pathParams = Params.from(path)
    }

    fun testGetWithHeader(headers: Map<String, String>) = get<Response> {
        url = "/response-headers"
        headersParams = Params.from(headers)
    }

    fun testGetWithHeaderAndQuery(headers: Map<String, String>, queries: Map<String,String>) = get<Response> {
        url = "/response-headers-queries"
        headersParams = Params.from(headers)
        queriesParams = Params.from(queries)
    }

    fun testPost(body: Params) = post<Response> {
        url = "/response-body"
        bodyParams = body
    }

    fun testPostWithModel(model: RequestModel) = post<Response>{
        url = "/response-body"
        bodyModel = model
    }

    fun testPostWithJsonModel(model: RequestModel) = jsonPost<Response>{
        url = "/response-body-with-model"
        jsonModel = model
    }

    fun testPostWithResponseMapper(model: RequestModel) = jsonPost<ResponseData>{
        url = "/response-body-with-model"
        jsonModel = model
        responseMapper = ResponseDataMapper::class
    }
}

定義好 ApiService 就可以直接使用了,例如:

val apiService by lazy {
    ApiService(httpClient)
}

val requestModel = RequestModel()
apiService.testPostWithModel(requestModel).sync()

當然也支持異步躁愿,會返回CompletableFuture對象叛本,例如:

val apiService by lazy {
    ApiService(httpClient)
}

val requestModel = RequestModel()
apiService.testPostWithModel(requestModel).async()

借助于 Kotlin 擴展函數(shù)的特性,也支持返回 RxJava 的 Observable 對象等彤钟、Reactor 的 Flux/Mono 對象来候、Kotlin Coroutines 的 Flow 對象等等。

三. Interceptors

okhttp-extension框架帶有很多常用的攔截器

3.1 CurlLoggingInterceptor

將網(wǎng)絡(luò)請求轉(zhuǎn)換成 curl 命令的攔截器逸雹,便于后端同學(xué)調(diào)試排查問題营搅。

以下面的代碼為例:

    httpClient.get{

        url {
            url = "/response-headers-queries"

            "param1" to "value1"
            "param2" to "value2"
        }

        header {
            "key1" to "value1"
            "key2" to "value2"
        }
    }.use {
        println(it)
    }

添加了 CurlLoggingInterceptor 之后,打印結(jié)果如下:

curl:
╔══════════════════════════════════════════════════════════════════════════════════════════════════
║ curl -X GET -H "key1: value1" -H "key2: value2" "http://localhost:8080/response-headers-queries?param1=value1&param2=value2"
╚══════════════════════════════════════════════════════════════════════════════════════════════════

CurlLoggingInterceptor 默認使用 println 函數(shù)打印梆砸,可以使用相應(yīng)的日志框架進行替換转质。

3.2 SigningInterceptor

請求簽名的攔截器,支持對 query 參數(shù)進行簽名帖世。

const val TIME_STAMP = "timestamp"
const val NONCE = "nonce"
const val SIGN = "sign"

private val extraMap:MutableMap<String,String> = mutableMapOf<String,String>().apply {
    this[TIME_STAMP] = System.currentTimeMillis().toString()
    this[NONCE]  = UUID.randomUUID().toString()
}

private val signingInterceptor = SigningInterceptor(SIGN, extraMap, signer = {
    val paramMap = TreeMap<String, String>()
    val url = this.url

    for (name in url.queryParameterNames) {
        val value = url.queryParameterValues(name)[0]?:""
        paramMap[name] = value
    }

    //增加公共參數(shù)
    paramMap[TIME_STAMP] = extraMap[TIME_STAMP].toString()
    paramMap[NONCE]  = extraMap[NONCE].toString()

    //所有參數(shù)自然排序后拼接
    var paramsStr = join("",paramMap.entries
        .filter { it.key!= SIGN }
        .map { entry -> String.format("%s", entry.value) })

    //生成簽名
    sha256HMAC(updateAppSecret,paramsStr)
})

3.3 TraceIdInterceptor

需要實現(xiàn)TraceIdProvider接口

interface TraceIdProvider {

    fun getTraceId():String
}

TraceIdInterceptor 會將 traceId 放入 http header 中休蟹。

3.4 OAuth2Interceptor

需要實現(xiàn)OAuth2Provider接口

interface OAuth2Provider {

    fun getOauthToken():String

    /**
     * 刷新token
     * @return String?
     */
    fun refreshToken(): String?
}

OAuth2Interceptor 會將 token 放入 http header 中,如果 token 過期,會調(diào)用 refreshToken() 方法進行刷新 token鸡挠。

3.5 JWTInterceptor

需要實現(xiàn)JWTProvider接口

interface JWTProvider {

    fun getJWTToken():String

    /**
     * 刷新token
     * @return String?
     */
    fun refreshToken(): String?
}

JWTInterceptor 會將 token 放入 http header 中,如果 token 過期搬男,會調(diào)用 refreshToken() 方法進行刷新 token拣展。

3.6 LoggingInterceptor

可以使用我開發(fā)的okhttp-logging-interceptor將 http request、response 的數(shù)據(jù)格式化的輸出缔逛。

四. Coroutines

Coroutines 是 Kotlin 的特性备埃,我們使用okhttp-extension也可以很好地利用 Coroutines。

4.1 Coroutines

例如褐奴,最基本的使用

   "https://baidu.com".asyncGet()
       .await()
       .use {
           println(it)
       }

亦或者

        httpClient.asyncGet{

            url{
                url = "/response-headers-queries"

                "param1" to "value1"
                "param2" to "value2"
            }

            header {
                "key1" to "value1"
                "key2" to "value2"
            }
        }.await().use {
            println(it)
        }

以及

        httpClient.asyncPost{

            url {
                url = "/response-body"
            }

            header {
                "key1" to "value1"
                "key2" to "value2"
            }

            body("application/json") {
                json {
                    "key1" to "value1"
                    "key2" to "value2"
                    "key3" to "value3"
                }
            }
        }.await().use{
            println(it)
        }

asyncGet\asyncPost\asyncPut\asyncDelete\asyncHead\asyncPatch 函數(shù)在coroutines模塊中按脚,都是 HttpClient 的擴展函數(shù),會返回Deferred<Response>對象敦冬。

同樣辅搬,他們也是基于 DSL 的。

4.2 Flow

coroutines模塊也提供了 flowGet\flowPost\flowPut\flowDelete\flowHead\flowPatch 函數(shù)脖旱,也是 HttpClient 的擴展函數(shù)堪遂,會返回Flow<Response>對象。

例如:

        httpClient.flowGet{

            url{
                url = "/response-headers-queries"

                "param1" to "value1"
                "param2" to "value2"
            }

            header {
                "key1" to "value1"
                "key2" to "value2"
            }
        }.collect {
            println(it)
        }

或者

        httpClient.flowPost{

            url {
                url = "/response-body"
            }

            header {
                "key1" to "value1"
                "key2" to "value2"
            }

            body("application/json") {
                json {
                    "key1" to "value1"
                    "key2" to "value2"
                    "key3" to "value3"
                }
            }
        }.collect{
            println(it)
        }

五. WebSocket

OkHttp 本身支持 WebSocket 萌庆,因此okhttp-extension對 WebSocket 做了一些增強溶褪,包括重連、連接狀態(tài)的監(jiān)聽等践险。

5.1 Reconnect

在實際的應(yīng)用場景中猿妈,WebSocket 的斷線是經(jīng)常發(fā)生的。例如:網(wǎng)絡(luò)發(fā)生切換巍虫、服務(wù)器負載過高無法響應(yīng)等都可能是 WebSocket 的斷線的原因彭则。

客戶端一旦感知到長連接不可用,就應(yīng)該發(fā)起重連占遥。okhttp-extension的 ReconnectWebSocketWrapper 類是基于 OkHttp 的 WebSocket 實現(xiàn)的包裝類贰剥,具有自動重新連接的功能。

在使用該包裝類時筷频,可以傳入自己實現(xiàn)的 WebSocketListener 來監(jiān)聽 WebSocket 各個狀態(tài)以及對消息的接收蚌成,該類也支持對 WebSocket 連接狀態(tài)變化的監(jiān)聽、支持設(shè)置重連的次數(shù)和間隔凛捏。

例如:

    // 支持重試的 WebSocket 客戶端
    ws = httpClient.websocket("http://127.0.0.1:9876/ws",listener = object : WebSocketListener() {

        override fun onOpen(webSocket: WebSocket, response: Response) {
            logger.info("connection opened...")

            websocket = webSocket

            disposable = Observable.interval(0, 15000,TimeUnit.MILLISECONDS) // 每隔 15 秒發(fā)一次業(yè)務(wù)上的心跳
                .subscribe({
                    heartbeat()
                }, {

                })
        }

        override fun onMessage(webSocket: WebSocket, text: String) {
            logger.info("received instruction: $text")
        }

        override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
        }

        override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
            logger.info("connection closing: $code, $reason")

            websocket = null

            disposable?.takeIf { !it.isDisposed }?.let {
                it.dispose()
            }
        }

        override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
            logger.error("connection closed: $code, $reason")

            websocket = null

            disposable?.takeIf { !it.isDisposed }?.let {
                it.dispose()
            }
        }

        override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
            logger.error("websocket connection error")

            websocket = null

            disposable?.takeIf { !it.isDisposed }?.let {
                it.dispose()
            }
        }
    },wsConfig = WSConfig())

5.2 onConnectStatusChangeListener

ReconnectWebSocketWrapper 支持對 WebSocket 連接狀態(tài)的監(jiān)聽担忧,只要實現(xiàn)onConnectStatusChangeListener即可。

    ws?.onConnectStatusChangeListener = {
        logger.info("${it.name}")
        status = it
    }

未完待續(xù)坯癣。
另外瓶盛,如果你對 Kotlin 比較感興趣,歡迎去我的新書《Kotlin 進階實戰(zhàn)》去看看,剛剛在 10 月出版惩猫,書中融入了我多年使用 Kotlin 的實踐思考與經(jīng)驗積累芝硬。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市轧房,隨后出現(xiàn)的幾起案子拌阴,更是在濱河造成了極大的恐慌,老刑警劉巖奶镶,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迟赃,死亡現(xiàn)場離奇詭異,居然都是意外死亡厂镇,警方通過查閱死者的電腦和手機纤壁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捺信,“玉大人酌媒,你說我怎么就攤上這事∑浚” “怎么了馍佑?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長梨水。 經(jīng)常有香客問我拭荤,道長,這世上最難降的妖魔是什么疫诽? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任舅世,我火速辦了婚禮,結(jié)果婚禮上奇徒,老公的妹妹穿的比我還像新娘雏亚。我一直安慰自己,他們只是感情好摩钙,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布罢低。 她就那樣靜靜地躺著,像睡著了一般胖笛。 火紅的嫁衣襯著肌膚如雪网持。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天长踊,我揣著相機與錄音功舀,去河邊找鬼。 笑死身弊,一個胖子當著我的面吹牛辟汰,可吹牛的內(nèi)容都是我干的列敲。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼帖汞,長吁一口氣:“原來是場噩夢啊……” “哼戴而!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起翩蘸,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤所意,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鹿鳖,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扁眯,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡壮莹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年翅帜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片命满。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡涝滴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出胶台,到底是詐尸還是另有隱情歼疮,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布诈唬,位于F島的核電站韩脏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏铸磅。R本人自食惡果不足惜赡矢,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望阅仔。 院中可真熱鬧吹散,春花似錦、人聲如沸八酒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽羞迷。三九已至界轩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間衔瓮,已是汗流浹背耸棒。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留报辱,地道東北人与殃。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓单山,卻偏偏與公主長得像,于是被迫代替她去往敵國和親幅疼。 傳聞我的和親對象是個殘疾皇子米奸,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

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