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惠昔,不依賴其他第三方庫
一. 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¶m2=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)驗積累芝硬。