Callback "hell" and response pattern

For more information about the method based on reaction stream and how to avoid callback hell.

A better understanding of the usefulness of one reaction stream-based methods is how it simplifies the non-blocking IO calls.

This article will briefly describe the type of synchronous remote call code involved. Then, we will demonstrate how hierarchical non-blocking IO in the efficient use of resources (especially the threads), introduced called callback hell complexity and how to respond flow method based on a simplified programming model brings.

1. The target service

The client calls represent the target service details city has two ports. When the type is used - when / cityids the URI calling, it returns a list of city id, and examples of the results are as follows:

[
    1,
    2,
    3,
    4,
    5,
    6,
    7
]
复制代码

A return port details given its ID of the city, for example, when using ID 1 - When calling "/ cities / 1":

{
    "country": "USA",
    "id": 1,
    "name": "Portland",
    "pop": 1600000
}
复制代码

The client's responsibility is to obtain a list of city ID, and then for each city, for detailed information about the city based on a combination of their ID and the city list.

2. synchronous call

I am using the Spring Framework RestTemplate remote calls. Get a list of Kotlin cityId functions as follows:

private fun getCityIds(): List<String> {
    val cityIdsEntity: ResponseEntity<List<String>> = restTemplate
            .exchange("http://localhost:$localServerPort/cityids",
                    HttpMethod.GET,
                    null,
                    object : ParameterizedTypeReference<List<String>>() {})
    return cityIdsEntity.body!!
}
复制代码

City get the details:

private fun getCityForId(id: String): City {
    return restTemplate.getForObject("http://localhost:$localServerPort/cities/$id", City::class.java)!!
}
复制代码

Given these two functions, they are easy to mix, easy to return to the list of cities:

val cityIds: List<String> = getCityIds()
val cities: List<City> = cityIds
        .stream()
        .map<City> { cityId -> getCityForId(cityId) }
        .collect(Collectors.toList())
cities.forEach { city -> LOGGER.info(city.toString()) }
复制代码

The code is easy to understand; however, involved eight blocking calls:

  1. Get a list of seven cities ID, and then get detailed information about each city
  2. For detailed information on seven cities

Each call will be on a different thread.

3. The non-blocking IO callbacks

I will use AsyncHttpClient libraries for non-blocking IO calls.

When a remote call, AyncHttpClientreturn ListenableFuture type.

val responseListenableFuture: ListenableFuture<Response> = asyncHttpClient
                .prepareGet("http://localhost:$localServerPort/cityids")
                .execute()
复制代码

You may be attached to a callback ListenableFuturein response to the operation when available.

responseListenableFuture.addListener(Runnable {
    val response: Response = responseListenableFuture.get()
    val responseBody: String = response.responseBody
    val cityIds: List<Long> = objectMapper.readValue<List<Long>>(responseBody,
            object : TypeReference<List<Long>>() {})
    ....
}
复制代码

Given a list of cityIds, I want to get detailed information on the city, so from the response, I need to be more long-distance calls and callbacks for each additional call for more information about the city:

val responseListenableFuture: ListenableFuture<Response> = asyncHttpClient
        .prepareGet("http://localhost:$localServerPort/cityids")
        .execute()
responseListenableFuture.addListener(Runnable {
    val response: Response = responseListenableFuture.get()
    val responseBody: String = response.responseBody
    val cityIds: List<Long> = objectMapper.readValue<List<Long>>(responseBody,
            object : TypeReference<List<Long>>() {})
    cityIds.stream().map { cityId ->
        val cityListenableFuture = asyncHttpClient
                .prepareGet("http://localhost:$localServerPort/cities/$cityId")
                .execute()
        cityListenableFuture.addListener(Runnable {
            val cityDescResp = cityListenableFuture.get()
            val cityDesc = cityDescResp.responseBody
            val city = objectMapper.readValue(cityDesc, City::class.java)
            LOGGER.info("Got city: $city")
        }, executor)
    }.collect(Collectors.toList())
}, executor)
复制代码

This is a rough code; callback and contains a set of callbacks, and the reasoning is difficult to understand - so it is called a "callback hell."

4. Use non-blocking IO in Java CompletableFuture in

By the Java CompletableFuture as a return type instead ListenableFuture return, this code can be slightly improved. CompletableFutureProvided that allow to modify and return type of operator.

For example, consider the function of acquiring city ID list:

private fun getCityIds(): CompletableFuture<List<Long>> {
    return asyncHttpClient
            .prepareGet("http://localhost:$localServerPort/cityids")
            .execute()
            .toCompletableFuture()
            .thenApply { response ->
                val s = response.responseBody
                val l: List<Long> = objectMapper.readValue(s, object : TypeReference<List<Long>>() {})
                l
            }
}
复制代码

Here, I use the thenApplyoperator to CompletableFuture<Response>convert CompletableFuture<List<Long>>.

Similarly, access to city details:

private fun getCityDetail(cityId: Long): CompletableFuture<City> {
    return asyncHttpClient.prepareGet("http://localhost:$localServerPort/cities/$cityId")
            .execute()
            .toCompletableFuture()
            .thenApply { response ->
                val s = response.responseBody
                LOGGER.info("Got {}", s)
                val city = objectMaper.readValue(s, City::class.java)
                city
            }
}
复制代码

This is an improvement method based callback. However, in this particular case, CompletableFuturethe lack of useful operators, for example, all cities need to put together the details:

val cityIdsFuture: CompletableFuture<List<Long>> = getCityIds()
val citiesCompletableFuture: CompletableFuture<List<City>> =
        cityIdsFuture
                .thenCompose { l ->
                    val citiesCompletable: List<CompletableFuture<City>> =
                            l.stream()
                                    .map { cityId ->
                                        getCityDetail(cityId)
                                    }.collect(toList())
                    val citiesCompletableFutureOfList: CompletableFuture<List<City>> = CompletableFuture.allOf(*citiesCompletable.toTypedArray())
                                    .thenApply { _: Void? ->
                                        citiesCompletable
                                                .stream()
                                                .map { it.join() }
                                                .collect(toList())
                                    }
                    citiesCompletableFutureOfList
                }
复制代码

Use of a called CompletableFuture.allOf operator, which returns a "Void" type, and must be forced back to the required type CompletableFuture<List<City>>.

5. Reactor Project

Project Reactor is Reactive Streams achieve specification. There are two special types of items may be returned stream and the stream 0/1 0 / n entries - the former is Mono, which is Flux.

Project Reactor provides a very rich set of operators, allowing data streams converted in various manners. First, consider returning the city ID list of functions:

private fun getCityIds(): Flux<Long> {
    return webClient.get()
            .uri("/cityids")
            .exchange()
            .flatMapMany { response ->
                LOGGER.info("Received cities..")
                response.bodyToFlux<Long>()
            }
}
复制代码

I am using Spring good WebClient Remote library calls and access to Project Reactor Mono <ClientResponse>type of response, you can use the flatMapManyoperator to modify it to Flux<Long>type.

According to city ID, for details of the city along the same route:

private fun getCityDetail(cityId: Long?): Mono<City> {
    return webClient.get()
            .uri("/cities/{id}", cityId!!)
            .exchange()
            .flatMap { response ->
                val city: Mono<City> = response.bodyToMono()
                LOGGER.info("Received city..")
                city
            }
}
复制代码

Here, Project Reactor Mono<ClientResponse>type being used flatMapoperator is converted to Mono<City>type.

And derive cityIds, this is the City's Code:

val cityIdsFlux: Flux<Long> = getCityIds()
val citiesFlux: Flux<City> = cityIdsFlux
        .flatMap { this.getCityDetail(it) }
return citiesFlux
复制代码

This is very expressive - confusion contrast method based callback and simplicity Reactive Streams-based methods.

6. Conclusion

In my opinion, this is one of the biggest reasons for using a method based on reaction stream, in particular Project Reactor, used for scenes involve crossing asynchronous boundaries, such as a remote call in this instance. It cleared the callback and callback confusion, provides a wealth of using operators to modify / convert types of natural methods.

All working version of the sample repository used herein are available in GitHub found on.

Original: dzone.com/articles/ca...

Author: Biju Kunjummen

Translator: Emma

Guess you like

Origin juejin.im/post/5d1579aee51d45772a49ad77