Implementation of easy-to-use and powerful network framework based on kotlin + okhttp (I)

Time:2022-5-14

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 httpGET/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
Implementation of easy-to-use and powerful network framework based on kotlin + okhttp (I)

okhttp-extension.png

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-extensionFeatures 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 returnCompletableFutureObject, 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-extensionThe 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&param2=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 implementTraceIdProviderInterface

interface TraceIdProvider {

    fun getTraceId():String
}

Traceidinterceptor will put traceid into HTTP header.

3.4 OAuth2Interceptor

Need to implementOAuth2ProviderInterface

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 implementJWTProviderInterface

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-extensionYou 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 functioncoroutinesModules are all extension functions of httpclient, which will returnDeferred<Response>Object.

Similarly, they are also based on DSL.

4.2 Flow

coroutinesThe 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-extensionSome 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-extensionReconnectwebsocketwrapper 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 implementedonConnectStatusChangeListenerJust.

    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.

Recommended Today

Security problems of JSP Application

1、 OverviewWhen network programming becomes more and more convenient, the system function becomes more and more powerful, but the security decreases exponentially. I’m afraid this is the misfortune and sadness of network programming. Various dynamic content generation environments have prospered www. their design goal is to give developers more power and end users more convenience. […]