回调“地狱”与反应模式


点击左上角蓝字,关注“锅外的大佬”640?wx_fmt=gif

专注分享国外最新技术内容

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

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

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

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<Response> 转换为 CompletableFuture<List<Long>>

同样的,获取城市详情:

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<List<City>>

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 <ClientResponse> 类型的响应,可以使用 flatMapMany 运算符将其修改为 Flux<Long> 类型。

根据城市 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<ClientResponse> 类型正在使用 flatMap 运算符转换为 Mono<City> 类型。

以及从中获取 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 

推荐阅读:

如何排查Java内存泄漏?看完我给跪了!

上篇好文:

Java动态规划


文章对你是否有帮助呢?

别忘记右上角按钮分享给更多人哦smiley_63.png~


640?wx_fmt=png


点击在看,和我一起帮助更多开发者!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值