okhttp-extension 是针对 okhttp 3 增强的网络框架。使用 Kotlin 特性编写,提供便捷的 DSL 方式创建网络请求,支持协程、响应式编程等等。
其 core 模块只依赖 OkHttp,不会引入第三方库。
okhttp-extension 可以整合 Retrofit、Feign 框架,还提供了很多常用的拦截器。 另外,okhttp-extension 也给开发者提供一种新的选择。
github地址:https://github.com/fengzhizi715/okhttp-extension
1Features:支持 DSL 创建 HTTP GET/POST/PUT/HEAD/DELETe/PATCH requests.
支持 Kotlin 协程
支持响应式(RxJava、Spring Reactor)
支持函数式
支持熔断器(Resilience4j)
支持异步请求的取消
支持 Request、Response 的拦截器
提供常用的拦截器
支持自定义线程池
支持整合 Retrofit、Feign 框架
支持 Websocket 的实现、自动重连等
core 模块只依赖 OkHttp,不依赖其他第三方库
无需任何配置(零配置)即可直接使用,仅限于 Get 请求。
"https://baidu.com".httpGet().use {
println(it)
}
或者需要依赖协程,也仅限于 Get 请求。
"https://baidu.com".asyncGet()
.await()
.use {
println(it)
}
31.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)
}
41.3 AOP这里的 url 需要和 baseUrl 组成完整的 url。比如:http://localhost:8080/response-headers-queries 当然,也可以使用 customUrl 替代 baseUrl + url 作为完整的 url
针对所有 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
Part2二. DSL我们可以多次调用 addRequestProcessor() 、addResponseProcessor() 方法。
DSL 是okhttp-extension框架的特色。包含使用 DSL 创建各种 HTTP Request 和使用 DSL 结合声明式编程。
52.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)
}
2.1.2 post这里的 url 需要和 baseUrl 组成完整的 url。比如:http://localhost:8080/response-headers-queries 当然,也可以使用 customUrl 替代 baseUrl + url 作为完整的 url
基本的 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)
}
2.1.3 put更多 post 相关的方法,欢迎使用者自行探索。
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)
}
62.2 Declarative
像使用 Retrofit、Feign 一样,在配置完 httpClient 之后,需要定义一个 ApiService 它用于声明所调用的全部接口。ApiService 所包含的方法也是基于 DSL 的。例如:
class ApiService(client: HttpClient) : AbstractHttpService(client) {
fun testGet(name: String) = get {
url = "/sayHi/$name"
}
fun testGetWithPath(path: Map) = get {
url = "/sayHi/{name}"
pathParams = Params.from(path)
}
fun testGetWithHeader(headers: Map) = get {
url = "/response-headers"
headersParams = Params.from(headers)
}
fun testGetWithHeaderAndQuery(headers: Map, queries: Map) = get {
url = "/response-headers-queries"
headersParams = Params.from(headers)
queriesParams = Params.from(queries)
}
fun testPost(body: Params) = post {
url = "/response-body"
bodyParams = body
}
fun testPostWithModel(model: RequestModel) = post{
url = "/response-body"
bodyModel = model
}
fun testPostWithJsonModel(model: RequestModel) = jsonPost{
url = "/response-body-with-model"
jsonModel = model
}
fun testPostWithResponseMapper(model: RequestModel) = jsonPost{
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 对象等等。
Part3三. Interceptorsokhttp-extension框架带有很多常用的拦截器
73.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¶m2=value2" ╚══════════════════════════════════════════════════════════════════════════════════════════════════
CurlLoggingInterceptor 默认使用 println 函数打印,可以使用相应的日志框架进行替换。
83.2 SigningInterceptor请求签名的拦截器,支持对 query 参数进行签名。
const val TIME_STAMP = "timestamp" const val NONCE = "nonce" const val SIGN = "sign" private val extraMap:MutableMap93.3 TraceIdInterceptor= mutableMapOf ().apply { this[TIME_STAMP] = System.currentTimeMillis().toString() this[NONCE] = UUID.randomUUID().toString() } private val signingInterceptor = SigningInterceptor(SIGN, extraMap, signer = { val paramMap = TreeMap () 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) })
需要实现TraceIdProvider接口
interface TraceIdProvider {
fun getTraceId():String
}
TraceIdInterceptor 会将 traceId 放入 http header 中。
103.4 OAuth2Interceptor需要实现OAuth2Provider接口
interface OAuth2Provider {
fun getOauthToken():String
fun refreshToken(): String?
}
OAuth2Interceptor 会将 token 放入 http header 中,如果 token 过期,会调用 refreshToken() 方法进行刷新 token。
113.5 JWTInterceptor需要实现JWTProvider接口
interface JWTProvider {
fun getJWTToken():String
fun refreshToken(): String?
}
JWTInterceptor 会将 token 放入 http header 中,如果 token 过期,会调用 refreshToken() 方法进行刷新 token。
123.6 LoggingInterceptor可以使用我开发的okhttp-logging-interceptor(https://github.com/fengzhizi715/okhttp-logging-interceptor)将 http request、response 的数据格式化的输出。
Part4四. CoroutinesCoroutines 是 Kotlin 的特性,我们使用okhttp-extension也可以很好地利用 Coroutines。
134.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)
}
asyncGetasyncPostasyncPutasyncDeleteasyncHeadasyncPatch 函数在coroutines模块中,都是 HttpClient 的扩展函数,会返回Deferred
同样,他们也是基于 DSL 的。
144.2 Flowcoroutines模块也提供了 flowGetflowPostflowPutflowDeleteflowHeadflowPatch 函数,也是 HttpClient 的扩展函数,会返回Flow
例如:
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)
}
Part5五. WebSocket
OkHttp 本身支持 WebSocket ,因此okhttp-extension对 WebSocket 做了一些增强,包括重连、连接状态的监听等。
155.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())
165.2 onConnectStatusChangeListener
ReconnectWebSocketWrapper 支持对 WebSocket 连接状态的监听,只要实现onConnectStatusChangeListener即可。
ws?.onConnectStatusChangeListener = {
logger.info("${it.name}")
status = it
}
未完待续。 另外,如果你对 Kotlin 比较感兴趣,欢迎去我的新书《Kotlin 进阶实战》(https://item.jd.com/12972991.html)去看看,刚刚在 10 月出版,书中融入了我多年使用 Kotlin 的实践思考与经验积累。
【Java与Android技术栈】公众号
关注 Java/Kotlin 服务端、桌面端 、Android 、机器学习、端侧智能
更多精彩内容请关注:



