使用了多年go之后,再转到Java的世界,我一直对Reactive Extensions - Rx的存在感到十分困惑。
虽然不像原生js那样的callback hell,但这样的代码,难道就很好读咩?
private fun getCityDetail(cityId: Long?): Mono {
return webClient.get()
.uri("/cities/{id}", cityId!!)
.exchange()
.flatMap { response ->
val city: Mono = response.bodyToMono()
LOGGER.info("Received city..")
city
}
}
能不能解释一下flatMap跟map的不同?错误处理又怎么办?为什么似乎很多人在说这样的代码风格很好?很适合什么数据流式处理?
(flatMap跟map的区别当然很容易搞懂,上面只是举例说Rx有额外的“心智负担”。)
我就一直将信将疑,猜想可能是Java系在数据流式处理领域证明的Rx的威力,然后再将其应用到网络异步编程的领域来;总之就是,不明觉厉。
直到我深入使用Rx再认识了kotlin coroutine,才确信Rx就是Java在缺乏coroutine协程支持,搞不了async / await等之后的妥协方式,它比在js中使用promise去处理异步高明不了多少,Rx必然是等着被历史淘汰的!类似的淘汰,已经在c#、python甚至js等语言中出现过多次了。
以下的样例均取自kotlin团队的开发Team Lead Roman Elizarov在KotlinConf 2017中的演讲:
假设有这样的同步代码:
fun postItem(item: Item) {
val token = requestToken()
val post = createPost(token, item)
processPost(post)
}
这样的同步代码是存在堵塞的,并且只能通过增加线程来提高并发,而线程若是过千,程序则需要耗费大量的CPU去处理线程切换,进而造成性能低下。
解决的办法是采用异步来避免线程堵塞,然后通过异步调用callback来触发处理,但这会导致所谓的callback hell:
fun postItem(item: Item) {
requestTokenAsync { token ->
createPostAsync(token, item) { post ->
processPost(post)
}
}
}
我们可以看到每一层异步调用都会产生多一层的函数嵌套,缩进会非常难看;上面的代码实际上还是简化过的,如果加上错误处理,其可读性会更加糟糕。
而如果采用future / promise / Rx等风格的话,实际上也没有把情况改善多少:
fun postItem(item: Item) {
requestTokenAsync()
.thenCompose { token -> createPostAsync(token, item) }
.thenAccept { post -> processPost(post) }
}
从代码风格上看,它仅是讲callback hell中的函数嵌套、缩进给“拍平”;但取而代之的是各种链式调用,仿佛整个函数从此都可以“一行搞定”。
但我们在“一行代码”中堆积了过多逻辑,其维护性必然是迅速下降的,debug更会是个麻烦。每次去看Rx代码抛出来的异常堆栈,我都觉得很头痛。
而kotlin引入coroutine之后,代码则可以变成这样:
suspend fun postItem(item: Item) {
val token = requestToken()
val post = createPost(token, item)
processPost(post)
}
coroutine的版本,几乎跟最初同步代码的版本一模一样,仅仅只是函数签名前面多了一个新的关键字suspend。
错误处理也非常简单,就跟普通的同步代码一样,使用try / catch即可;而且性能也会于使用callbacak / Rx的异步代码一致。
debug也获得极大的改善,所有suspend函数的嵌套,会产生所谓的coroutine boundary,即便是在最内层抛出异常,整个coroutine堆栈都会被打印出来,跟同步代码差不多容易看懂。
kotlin coroutine包对于Java目前流行的Rx类库(reactive / reactor / rx2 / rx3等)也提供了内置的支持,我们可以很方便的将现有的Rx代码封装成为coroutine。
比方说,现有一个httpclient使用rx2返回数据:
@Client("\\${blogapi.url}")
interface BlogApi {
@Get(value = "/blog?id={blogId}")
fun blog(blogId: Int): Single
}
可以写个Service把这么个异步client封装起来:
@Singleton
class BlogService (val blogApi: BlogApi) {
suspend fun blog(blogId: Int): Blog {
return blogApi.blog(blogId).await()
}
}
其中blogApi.blog(blogId).await()是中的await 是kotlin coroutine库内置提供的rx2扩展方法。
那么,这样一来,相应的上层代码也可以做修改成为同步的风格。
从实现的角度讲,kotlin coroutine并没有在jvm层面做任何修改,简单的说,编译器只是识别suspend关键字的函数,然后进行CPS - Continuation-Passing Style转换,即把代码内部转换成为类似:
fun postItem(item: Item, cont: Continuation) {
val sm = object: CoroutineImpl{ ...}
switch(sm.label) {
case0:
sm.item= item
sm.label = 1
requestToken(sm)
case1:
createPost(token, item, sm)
case2:
processPost(post)
}
}
即自动构造了函数相应的sm - State Machine状态机,然后在发生callback的时候,重新调用函数,函数的“状态”会被保持在状态机中,而不同的状态也会使其执行不同的步骤。
状态机也是其它很多语言实现协程所选择的常见方式,而并不是kotlin独创,而kotlin coroutine在2017年在kotlinConf上被介绍到现在也过了3年,可以相信其成熟度。
Project Loom暂时依旧遥遥无期的2020年,Java程序员不应该再使用Rx,而应该拥抱kotlin,拥抱coroutine。