基于 Kotlin + OkHttp 实现易用且功能强大的网络框架(一)

简介: 基于 Kotlin + OkHttp 实现易用且功能强大的网络框架(一)

okhttp-extension 是针对 okhttp 3 增强的网络框架。使用 Kotlin 特性编写,提供便捷的 DSL 方式创建网络请求,支持协程、响应式编程等等。


其 core 模块只依赖 OkHttp,不会引入第三方库。


okhttp-extension 可以整合 Retrofit、Feign 框架,还提供了很多常用的拦截器。 另外,okhttp-extension 也给开发者提供一种新的选择。


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


Features:



  • 支持 DSL 创建 HTTP GET/POST/PUT/HEAD/DELETE/PATCH requests.
  • 支持 Kotlin 协程
  • 支持响应式(RxJava、Spring Reactor)
  • 支持函数式
  • 支持熔断器(Resilience4j)
  • 支持异步请求的取消
  • 支持 Request、Response 的拦截器
  • 提供常用的拦截器
  • 支持自定义线程池
  • 支持整合 Retrofit、Feign 框架
  • 支持 Websocket 的实现、自动重连等
  • core 模块只依赖 OkHttp,不依赖其他第三方库

image.png

okhttp-extension.png


一. General



1.1 Basic


无需任何配置(零配置)即可直接使用,仅限于 Get 请求。

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


或者需要依赖协程,也仅限于 Get 请求。

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


1.2 Config


配置 OkHttp 相关的参数以及拦截器,例如:

const val DEFAULT_CONN_TIMEOUT = 30
val loggingInterceptor by lazy {
    LogManager.logProxy(object : LogProxy {  // 必须要实现 LogProxy ,否则无法打印网络请求的 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: 发布到生产环境需要改成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 的行为。


需要在构造 httpClient 时,调用 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"。


因为在创建 request 之前,会处理所有的 RequestProcessor;在响应 response 之前,也会用内部的 ResponseProcessingInterceptor 拦截器来处理 ResponseProcessor。


RequestProcessor、ResponseProcessor 分别可以认为是 request、response 的拦截器。

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


我们可以多次调用 addRequestProcessor() 、addResponseProcessor() 方法。


二. DSL



DSL 是okhttp-extension框架的特色。包含使用 DSL 创建各种 HTTP Request 和使用 DSL 结合声明式编程。


2.1 HTTP Request


使用 DSL 支持创建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 相关的方法,欢迎使用者自行探索。


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 它用于声明所调用的全部接口。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 扩展函数的特性,也支持返回 RxJava 的 Observable 对象等、Reactor 的 Flux/Mono 对象、Kotlin Coroutines 的 Flow 对象等等。


三. Interceptors



okhttp-extension框架带有很多常用的拦截器


3.1 CurlLoggingInterceptor


将网络请求转换成 curl 命令的拦截器,便于后端同学调试排查问题。


以下面的代码为例:

httpClient.get{
        url {
            url = "/response-headers-queries"
            "param1" to "value1"
            "param2" to "value2"
        }
        header {
            "key1" to "value1"
            "key2" to "value2"
        }
    }.use {
        println(it)
    }


添加了 CurlLoggingInterceptor 之后,打印结果如下:

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


CurlLoggingInterceptor 默认使用 println 函数打印,可以使用相应的日志框架进行替换。


3.2 SigningInterceptor


请求签名的拦截器,支持对 query 参数进行签名。

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
    }
    //增加公共参数
    paramMap[TIME_STAMP] = extraMap[TIME_STAMP].toString()
    paramMap[NONCE]  = extraMap[NONCE].toString()
    //所有参数自然排序后拼接
    var paramsStr = join("",paramMap.entries
        .filter { it.key!= SIGN }
        .map { entry -> String.format("%s", entry.value) })
    //生成签名
    sha256HMAC(updateAppSecret,paramsStr)
})


3.3 TraceIdInterceptor


需要实现TraceIdProvider接口

interface TraceIdProvider {
    fun getTraceId():String
}


TraceIdInterceptor 会将 traceId 放入 http header 中。


3.4 OAuth2Interceptor


需要实现OAuth2Provider接口

interface OAuth2Provider {
    fun getOauthToken():String
    /**
     * 刷新token
     * @return String?
     */
    fun refreshToken(): String?
}


OAuth2Interceptor 会将 token 放入 http header 中,如果 token 过期,会调用 refreshToken() 方法进行刷新 token。


3.5 JWTInterceptor


需要实现JWTProvider接口

interface JWTProvider {
    fun getJWTToken():String
    /**
     * 刷新token
     * @return String?
     */
    fun refreshToken(): String?
}

JWTInterceptor 会将 token 放入 http header 中,如果 token 过期,会调用 refreshToken() 方法进行刷新 token。


3.6 LoggingInterceptor


可以使用我开发的okhttp-logging-interceptor将 http request、response 的数据格式化的输出。


四. 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 函数在coroutines模块中,都是 HttpClient 的扩展函数,会返回Deferred<Response>对象。


同样,他们也是基于 DSL 的。


4.2 Flow


coroutines模块也提供了 flowGet\flowPost\flowPut\flowDelete\flowHead\flowPatch 函数,也是 HttpClient 的扩展函数,会返回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 做了一些增强,包括重连、连接状态的监听等。


5.1 Reconnect


在实际的应用场景中,WebSocket 的断线是经常发生的。例如:网络发生切换、服务器负载过高无法响应等都可能是 WebSocket 的断线的原因。


客户端一旦感知到长连接不可用,就应该发起重连。okhttp-extension的 ReconnectWebSocketWrapper 类是基于 OkHttp 的 WebSocket 实现的包装类,具有自动重新连接的功能。


在使用该包装类时,可以传入自己实现的 WebSocketListener 来监听 WebSocket 各个状态以及对消息的接收,该类也支持对 WebSocket 连接状态变化的监听、支持设置重连的次数和间隔。


例如:

// 支持重试的 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 秒发一次业务上的心跳
                .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 连接状态的监听,只要实现onConnectStatusChangeListener即可。

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


相关文章
|
7天前
|
机器学习/深度学习 算法 PyTorch
基于图神经网络的大语言模型检索增强生成框架研究:面向知识图谱推理的优化与扩展
本文探讨了图神经网络(GNN)与大型语言模型(LLM)结合在知识图谱问答中的应用。研究首先基于G-Retriever构建了探索性模型,然后深入分析了GNN-RAG架构,通过敏感性研究和架构改进,显著提升了模型的推理能力和答案质量。实验结果表明,改进后的模型在多个评估指标上取得了显著提升,特别是在精确率和召回率方面。最后,文章提出了反思机制和教师网络的概念,进一步增强了模型的推理能力。
27 4
基于图神经网络的大语言模型检索增强生成框架研究:面向知识图谱推理的优化与扩展
|
25天前
|
人工智能 自然语言处理
WebDreamer:基于大语言模型模拟网页交互增强网络规划能力的框架
WebDreamer是一个基于大型语言模型(LLMs)的网络智能体框架,通过模拟网页交互来增强网络规划能力。它利用GPT-4o作为世界模型,预测用户行为及其结果,优化决策过程,提高性能和安全性。WebDreamer的核心在于“做梦”概念,即在实际采取行动前,用LLM预测每个可能步骤的结果,并选择最有可能实现目标的行动。
54 1
WebDreamer:基于大语言模型模拟网页交互增强网络规划能力的框架
|
1月前
|
机器学习/深度学习 运维 安全
图神经网络在欺诈检测与蛋白质功能预测中的应用概述
金融交易网络与蛋白质结构的共同特点是它们无法通过简单的欧几里得空间模型来准确描述,而是需要复杂的图结构来捕捉实体间的交互模式。传统深度学习方法在处理这类数据时效果不佳,图神经网络(GNNs)因此成为解决此类问题的关键技术。GNNs通过消息传递机制,能有效提取图结构中的深层特征,适用于欺诈检测和蛋白质功能预测等复杂网络建模任务。
67 2
图神经网络在欺诈检测与蛋白质功能预测中的应用概述
|
29天前
|
安全 网络安全 数据安全/隐私保护
利用Docker的网络安全功能来保护容器化应用
通过综合运用这些 Docker 网络安全功能和策略,可以有效地保护容器化应用,降低安全风险,确保应用在安全的环境中运行。同时,随着安全威胁的不断变化,还需要持续关注和研究新的网络安全技术和方法,不断完善和强化网络安全保护措施,以适应日益复杂的安全挑战。
42 5
|
27天前
|
存储 监控 数据挖掘
计算机网络的功能
计算机网络支持信息交换、资源共享、分布式处理、可靠性增强及集中管理。信息交换涵盖多种媒体形式,促进远程协作;资源共享降低用户成本,提高效率;分布式处理提升计算能力;冗余机制保障系统稳定;集中管理简化网络维护,确保安全运行。
32 2
|
1月前
|
JSON 数据处理 Swift
Swift 中的网络编程,主要介绍了 URLSession 和 Alamofire 两大框架的特点、用法及实际应用
本文深入探讨了 Swift 中的网络编程,主要介绍了 URLSession 和 Alamofire 两大框架的特点、用法及实际应用。URLSession 由苹果提供,支持底层网络控制;Alamofire 则是在 URLSession 基础上增加了更简洁的接口和功能扩展。文章通过具体案例对比了两者的使用方法,帮助开发者根据需求选择合适的网络编程工具。
29 3
|
1月前
|
安全 API 网络安全
使用OkHttp进行HTTPS请求的Kotlin实现
使用OkHttp进行HTTPS请求的Kotlin实现
|
1月前
|
网络协议 Unix Linux
精选2款C#/.NET开源且功能强大的网络通信框架
精选2款C#/.NET开源且功能强大的网络通信框架