回调“地狱”与反应模式

了解更多有关基于反应流的方法以及如何避免回调地狱的信息。java

更好地理解基于反应流的方法的有用性的方法之一是它如何简化非阻塞 IO 调用。react

本篇文章将简要介绍进行同步远程调用所涉及的代码类型。而后,咱们将演示非阻塞 IO 中的分层如何高效使用资源(尤为是线程),引入了称为回调地狱带来的复杂性以及基于反应流方法如何简化编程模型。web

1. 目标服务

客户端调用表示城市详细信息的目标服务有两个端口。当使用类型为——/cityids 的 URI 调用时,返回城市 id 列表,而且示例结果以下所示:面试

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

一个端口返回给定其 ID 的城市的详细信息,例如,当使用 ID 为1——“/cities/1” 调用时:算法

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

客户端的责任是获取城市 ID 的列表,而后对于每一个城市,根据 ID 获取城市的详细信息并将其组合到城市列表中。数据库

2. 同步调用

我正在使用 Spring Framework 的 RestTemplate 进行远程调用。获取 cityId 列表的 Kotlin 函数以下所示:编程

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!!}复制代码

获取城市详情:安全

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

鉴于这两个函数,它们很容易组合,以便于轻松返回城市列表 :服务器

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()) }复制代码

代码很容易理解;可是,涉及八个阻塞调用:并发

  1. 获取 7 个城市 ID 的列表,而后获取每一个城市的详细信息
  2. 获取 7 个城市的详细信息

每个调用都将在不一样的线程上。

3. 非阻塞 IO 回调

我将使用 AsyncHttpClient 库来进行非阻塞 IO 调用。

进行远程调用时,AyncHttpClient 返回 ListenableFuture 类型。

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

能够将回调附加到 ListenableFuture 以在可用时对响应进行操做。

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 的列表,我想得到城市的详细信息,所以从响应中,我须要进行更多的远程调用并为每一个调用附加回调以获取城市的详细信息:

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)复制代码

这是一段粗糙的代码;回调中又包含一组回调,很难推理和理解 - 所以它被称为“回调地狱”。

4. 在 Java CompletableFuture 中使用非阻塞 IO

经过将 Java 的 CompletableFuture 做为返回类型而不是 ListenableFuture 返回,能够稍微改进此代码。CompletableFuture 提供容许修改和返回类型的运算符。

例如,考虑获取城市 ID 列表的功能:

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            }}复制代码

在这里,我使用 thenApply 运算符将 CompletableFuture 转换为 CompletableFuture >

一样的,获取城市详情:

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            }}复制代码

这是基于回调的方法的改进。可是,在这个特定状况下,CompletableFuture 缺少有用的运算符,例如,全部城市细节都须要放在一块儿:

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                }复制代码

使用了一个名为 CompletableFuture.allOf 的运算符,它返回一个“Void”类型,而且必须强制返回所需类型的 CompletableFuture >

5. 使用 Reactor 项目

Project Reactor 是 Reactive Streams 规范的实现。它有两种特殊类型能够返回 0/1 项的流和 0/n 项的流 - 前者是 Mono,后者是 Flux。

Project Reactor 提供了一组很是丰富的运算符,容许以各类方式转换数据流。首先考虑返回城市 ID 列表的函数:

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

我正在使用 Spring 优秀的 WebClient 库进行远程调用并得到 Project Reactor Mono 类型的响应,可使用 flatMapMany 运算符将其修改成 Flux 类型。

根据城市 ID,沿着一样的路线获取城市的详情:

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            }}复制代码

在这里,Project Reactor Mono 类型正在使用 flatMap 运算符转换为 Mono 类型。

以及从中获取 cityIds,这是 City 的代码:

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

这很是具备表现力 - 对比基于回调的方法的混乱和基于 Reactive Streams 的方法的简单性。

6. 结束语

在我看来,这是使用基于反应流的方法的最大缘由之一,特别是 Project Reactor,用于涉及跨越异步边界的场景,例如在此实例中进行远程调用。它清理了回调和回调的混乱,提供了一种使用丰富的运算符进行修改/转换类型的天然方法。

本文使用的全部示例的工做版本的存储库均可以在 GitHub 上找到。

原文:https://dzone.com/articles/callback-hell-and-reactive-patterns

做者:Biju Kunjummen

译者:Emma

------

推荐阅读

img

相关文章
相关标签/搜索