Okhttp extension is an enhanced network framework for okhttp 3. Written with the kotlin feature, it provides a convenient DSL way to create network requests, and supports collaborative programming, responsive programming, etc.
Its core module only relies on okhttp and will not introduce third-party libraries.
Okhttp extension can integrate retrofit and feign frameworks, and also provides many commonly used interceptors. In addition, okhttp extension also provides developers with a new choice.
GitHub address:https://github.com/fengzhizi715/okhttp-extension
Features:
- Support DSL to create http
GET
/POST
/PUT
/HEAD
/DELETE
/PATCH
requests. - Support kotlin collaboration
- Support responsive (rxjava, spring reactor)
- Support function
- Support fuse (resilience4j)
- Supports the cancellation of asynchronous requests
- Interceptor supporting request and response
- Provide common interceptors
- Support custom thread pool
- Support the integration of retrofit and feign frameworks
- Support the implementation of websocket, automatic reconnection, etc
- The core module only relies on okhttp and does not rely on other third-party libraries

I General
1.1 Basic
It can be used directly without any configuration (zero configuration), which is limited to get requests.
"https://baidu.com".httpGet().use {
println(it)
}
Or it needs to rely on the cooperation process, which is also limited to get requests.
"https://baidu.com".asyncGet()
.await()
.use {
println(it)
}
1.2 Config
Configure okhttp related parameters and interceptors, for example:
const val DEFAULT_CONN_TIMEOUT = 30
val loggingInterceptor by lazy {
LogManager. Logproxy (object: logproxy {// logproxy must be implemented, otherwise the request and response of the network request cannot be printed
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: Publishing to the production environment needs to be changed to false
.request()
.requestTag("Request")
.response()
.responseTag("Response")
// . Hideverticalline() // hide vertical line border
.build()
}
val httpClient: HttpClient by lazy {
HttpClientBuilder()
.baseUrl("https://localhost:8080")
.allTimeouts(DEFAULT_CONN_TIMEOUT.toLong(), TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor)
.addInterceptor(CurlLoggingInterceptor())
.serializer(GsonSerializer())
.jsonConverter(GlobalRequestJSONConverter::class)
.build()
}
After configuration, you can directly use httpclient
httpClient.get{
url {
url = "/response-headers-queries"
"param1" to "value1"
"param2" to "value2"
}
header {
"key1" to "value1"
"key2" to "value2"
}
}.use {
println(it)
}
The URL here needs to form a complete URL with baseurl. For example:http://localhost:8080/response-headers-queries
Of course, you can also use customurl instead of baseurl + URL as the complete URL
1.3 AOP
Do some AOP like behaviors for all requests and responses.
When constructing httpclient, you need to call addrequestprocessor() and addresponseprocessor() methods, for example:
val httpClientWithAOP by lazy {
HttpClientBuilder()
.baseUrl("https://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()
}
In this way, “request start” and “response start” will be printed respectively during request and response.
Because all requestprocessors will be processed before creating a request; Before responding to the response, the internal responseprocessinginterceptor interceptor will also be used to process the responseprocessor.
Requestprocessor and responseprocessor can be considered as interceptors of request and response respectively.
// a request interceptor
typealias RequestProcessor = (HttpClient, Request.Builder) -> Request.Builder
// a response interceptor
typealias ResponseProcessor = (Response) -> Unit
We can call addrequestprocessor () and addresponseprocessor () methods multiple times.
II DSL
DSL isokhttp-extension
Features of the framework. It includes creating various HTTP requests using DSL and declarative programming using DSL.
2.1 HTTP Request
Using DSL support to createGET
/POST
/PUT
/HEAD
/DELETE
/PATCH
2.1.1 get
The most basic get usage
httpClient.get{
url {
url = "/response-headers-queries"
"param1" to "value1"
"param2" to "value2"
}
header {
"key1" to "value1"
"key2" to "value2"
}
}.use {
println(it)
}
The URL here needs to form a complete URL with baseurl. For example:http://localhost:8080/response-headers-queries
Of course, you can also use customurl instead of baseurl + URL as the complete URL
2.1.2 post
The basic post request is as follows:
httpClient.post{
url {
url = "/response-body"
}
header {
"key1" to "value1"
"key2" to "value2"
}
body {
form {
"form1" to "value1"
"form2" to "value2"
}
}
}.use {
println(it)
}
Support request body as JSON string
httpClient.post{
url {
url = "/response-body"
}
body("application/json") {
json {
"key1" to "value1"
"key2" to "value2"
"key3" to "value3"
}
}
}.use {
println(it)
}
Support the upload of single / multiple files
val file = File("/Users/tony/Downloads/xxx.png")
httpClient.post{
url {
url = "/upload"
}
multipartBody {
+part("file", file.name) {
file(file)
}
}
}.use {
println(it)
}
For more post related methods, users are welcome to explore by themselves.
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
Like using retrofit and feign, after configuring httpclient, you need to define an apiservice, which is used to declare all the interfaces called. The methods contained in apiservice are also based on DSL. For example:
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
}
}
Once the apiservice is defined, it can be used directly, for example:
val apiService by lazy {
ApiService(httpClient)
}
val requestModel = RequestModel()
apiService.testPostWithModel(requestModel).sync()
Of course, it also supports asynchrony and will returnCompletableFuture
Object, for example:
val apiService by lazy {
ApiService(httpClient)
}
val requestModel = RequestModel()
apiService.testPostWithModel(requestModel).async()
With kotlinspread function
It also supports the return of observable objects of rxjava, flux / mono objects of reactor, flow objects of kotlin coroutines, etc.
III Interceptors
okhttp-extension
The framework has many commonly used interceptors
3.1 CurlLoggingInterceptor
The interceptor that converts the network request into curl command is convenient for the back-end students to debug and troubleshoot problems.
Take the following code as an example:
httpClient.get{
url {
url = "/response-headers-queries"
"param1" to "value1"
"param2" to "value2"
}
header {
"key1" to "value1"
"key2" to "value2"
}
}.use {
println(it)
}
After adding the curllogginginterceptor, the print results are as follows:
curl:
╔══════════════════════════════════════════════════════════════════════════════════════════════════
║ curl -X GET -H "key1: value1" -H "key2: value2" "http://localhost:8080/response-headers-queries?param1=value1¶m2=value2"
╚══════════════════════════════════════════════════════════════════════════════════════════════════
By default, the curllogginginterceptor uses the println function to print, which can be replaced by the corresponding log framework.
3.2 SigningInterceptor
The interceptor requesting signature supports signing query parameters.
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
}
//Add public parameters
paramMap[TIME_STAMP] = extraMap[TIME_STAMP].toString()
paramMap[NONCE] = extraMap[NONCE].toString()
//All parameters are spliced after natural sorting
var paramsStr = join("",paramMap.entries
.filter { it.key!= SIGN }
.map { entry -> String.format("%s", entry.value) })
//Generate signature
sha256HMAC(updateAppSecret,paramsStr)
})
3.3 TraceIdInterceptor
Need to implementTraceIdProvider
Interface
interface TraceIdProvider {
fun getTraceId():String
}
Traceidinterceptor will put traceid into HTTP header.
3.4 OAuth2Interceptor
Need to implementOAuth2Provider
Interface
interface OAuth2Provider {
fun getOauthToken():String
/**
*Refresh token
* @return String?
*/
fun refreshToken(): String?
}
Oauth2interceptor will put the token into the HTTP header. If the token expires, it will call the refreshtoken () method to refresh the token.
3.5 JWTInterceptor
Need to implementJWTProvider
Interface
interface JWTProvider {
fun getJWTToken():String
/**
*Refresh token
* @return String?
*/
fun refreshToken(): String?
}
Jwtinterceptor will put the token into the HTTP header. If the token expires, it will call the refreshtoken () method to refresh the token.
3.6 LoggingInterceptor
Can use my developmentokhttp-logging-interceptorOutput formatted data of HTTP request and response.
IV Coroutines
Coroutines is a feature of kotlin, which we useokhttp-extension
You can also make good use of coroutines.
4.1 Coroutines
For example, the most basic use
"https://baidu.com".asyncGet()
.await()
.use {
println(it)
}
Or
httpClient.asyncGet{
url{
url = "/response-headers-queries"
"param1" to "value1"
"param2" to "value2"
}
header {
"key1" to "value1"
"key2" to "value2"
}
}.await().use {
println(it)
}
as well as
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 functioncoroutines
Modules are all extension functions of httpclient, which will returnDeferred<Response>
Object.
Similarly, they are also based on DSL.
4.2 Flow
coroutines
The module also provides the flowget \ flowpost \ flowput \ flowdelete \ flowhead \ flowpatch function, which is also an extension function of httpclient and will returnFlow<Response>
Object.
For example:
httpClient.flowGet{
url{
url = "/response-headers-queries"
"param1" to "value1"
"param2" to "value2"
}
header {
"key1" to "value1"
"key2" to "value2"
}
}.collect {
println(it)
}
perhaps
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)
}
V WebSocket
Okhttp itself supports websocket, sookhttp-extension
Some enhancements have been made to websocket, including reconnection and monitoring of connection status.
5.1 Reconnect
In the actual application scenario, the disconnection of websocket often occurs. For example, network switching, high server load and inability to respond may be the reasons for websocket disconnection.
Once the client perceives that the long connection is unavailable, it should initiate reconnection.okhttp-extension
Reconnectwebsocketwrapper class is a wrapper class implemented by websocket based on okhttp, which has the function of automatic reconnection.
When using this wrapper class, you can pass in your own websocketlistener to monitor the status of WebSockets and receive messages. This class also supports monitoring the change of websocket connection status and setting the number and interval of reconnection.
For example:
//Websocket client that supports retry
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) // send a heartbeat every 15 seconds
.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 supports listening to websocket connection status as long as it is implementedonConnectStatusChangeListener
Just.
ws?.onConnectStatusChangeListener = {
logger.info("${it.name}")
status = it
}
To be continued.
In addition, if you are interested in kotlin, welcome to my new bookKotlin advanced combatJust published in October, the book integrates my practical thinking and experience of using kotlin for many years.