Kotlin Coroutine 是如何与 Spring WebFlux 整合的

一、前言

上篇文章介绍了 Kotlin Coroutine 的实现原理。因为篇幅所限,并未介绍 Kotlin Coroutine 具体是如何与其它异步编程技术整合的。本文将向大家介绍 Kotlin Coroutine 是如何与 Spring Reactor 整合。

虽然本文的标题是关于 Kotlin Coroutine 与 Spring WebFlux 的,但其实讲的是 Kotlin Coroutine 是如何与 Spring Reactor 整合的。因为 Spring Reactor 是 Spring WebFlux 的基础,所以不管起哪个标题,内容都是类似的。

因为 Spring WebFlux 这个名字更容易吸引人,所以本文便做了回标题党。

Kotlin Coroutine 与 Spring Reactor 的整合主要是通过 kotlinx-coroutines-reactivekotlinx-coroutines-reactor 实现的。本文提到的源码都能从这两个模块中找到。

二、示例

本文将继续使用《Kotlin Coroutine 初探》一文中的在 Spring WebFlux 中使用 Kotlin Coroutine 的示例:

@GetMapping("/coroutine/{personId}")
fun getNumberOfMessages(@PathVariable personId: String) = mono(Unconfined) { // ①
    val person = peopleRepository.findById(personId).awaitFirstOrDefault(null) // ②
            ?: throw NoSuchElementException("No person can be found by $personId")

    // ②
    val lastLoginDate = auditRepository.findByEmail(person.email).awaitSingle().eventDate

    val numberOfMessages =
            messageRepository.countByMessageDateGreaterThanAndEmail(lastLoginDate, person.email).awaitSingle() // ②

    "Hello ${person.name}, you have $numberOfMessages messages since $lastLoginDate"
}

本文将重点介绍上面代码标注的 ①、② 两点。

第 ① 点:mono(Unconfined) { ... } 的实现原理

第 ② 点:awaitXXX 方法的实现原理

理解了上面两点,就能理解 Kotlin Coroutine 是如何与 Spring Reactor 整合的了。

三、Spring Reactor 相关知识

本节会介绍一下后面会涉及到的 Spring Reactor 的概念和实现细节方面的内容。之所以将 Spring Reactor 的部分内容单拎出来,是因为预先了解 Spring Reactor 的部分内容对理解后面的内容非常重要。

1. Publisher/Mono/Flux

Reactive Streams 是 Spring、Netflix 等公司提出的一个反应式编程的一个规范。这个规范定义了必要的接口和对实现的要求。Publisher 是其中一个重要的接口:

public interface Publisher<T> {
    public void subscribe(Subscriber<? super T> s);
}

Publisher 顾名思义,发布者,等同于很多技术中的 Observablesubscribe 方法实际应该被看做是被订阅,即 aPublisher.subscribe(aSubscriber) 应该理解为 aPublisheraSubscriber 订阅。

但不同于之前同样有订阅机制的技术,比方说一些消息队列。之前的订阅机制中,订阅和消息的发布是两个独立的环节。而 Publisher.subscribe(Subscriber) 方法,如果被调用,则会使这个 Publisher 开始发布消息。

MonoFlux 是在实现了 Reactive Streams 规范的 Spring Reactor 中的两个类。这两个类实现了 Publisher 接口。不同于类似的 RxJava 2,其只有一个 Publisher 的实现类。Spring Reactor 中两个 Publisher 实现类,分别代表单个元素和多个元素两种场景。

在使用了 Spring WebFlux(基于 Spring Reactor 的新一代 Web 框架)的项目中,一个被 @RequestMapping 标注的方法需要返回 MonoFlux。然后,当一个请求根据映射配置被转发到这个方法上时,一个 MonoFlux 对象会根据这个方法的定义创建出一个 Mono。但这时真正的请求处理并未开始,方法返回的只是一个处理的步骤定义。

当 Spring WebFlux 框架得到这个方法返回的 MonoFlux 之后,会调用它们的 subscribe(Subcriber) 方法。此时,真正的请求处理便开始了。

后面的内容为了简便,会省略 Flux,仅会提到 Mono,但两者的原理基本类似。

2. Mono.create(Consumer<MonoSink<T>>) 方法

Spring WebFlux 和传统 Spring MVC 最大的不同就是要求方法返回 MonoFlux。当 Spring WebFlux 与 Kotlin Coroutine 整合后,我们需要将 Coroutine 转换成一个 Mono(或者 Flux,后面将省略 Flux)。

如何做呢?Kotlin Coroutine 使用的是 Mono.create(Consumer<MonoSink<T>>) 方法。

从方法签名看,Mono.create 方法涉及到最主要的接口是 MonoSink(此处不解释 Consumer 接口)。

MonoSink 是什么呢?其 API 文档是这么解释的:

Wrapper API around an actual downstream Subscriber for emitting nothing, a single value or an error (mutually exclusive).

简单理解就是对后续 Subscriber 的封装。

可能有些同学对 Sink 这个词有些陌生,我起初也是这种感觉。但是对于一些做过流处理相关开发的同学,这个词应该不陌生。原因是 Sink 这个词经常出现在流处理相关技术中(比如 Flink、Flume)。在 Spring Cloud Stream 中,也能看到这个词。同样,Spring Reactor 是对 Reactive Streams 规范的实现,也可以看做是另一种形式的流技术,所以,出现 Sink 这个词也不足为奇了。

因为 MonoSink 是对后续 Subscriber 的封装,所以可以利用 MonoSink 向后续的 Subscriber 输出一些东西的。在 Kotlin Coroutine 与 Spring Reactor 整合的过程中,Kotlin Coroutine 将开启一个 Coroutine,并将执行结果通过 MonoSink 输出给 Subscriber。

在 Spring WebFlux 应用中,Subscriber 会将 Mono(或 Flux)以 HTTP 数据的形式输出。

这样就完成了 Kotlin Coroutine 向 Mono 转换的主要工作。更多细节将在下面的内容介绍。

四、整合的两个关键点

接下来将向大家介绍 Kotlin Coroutine 与 Spring Reactor整合的两个关键点:mono 方法和 await 系列方法。

1. mono 方法

mono 方法连接了 Spring Reactor 环境与 Kotlin Coroutine 环境,可以看做是一个将 Kotlin Coroutine 装换为 Spring Reactor Mono 的工厂方法。我们先来看 mono 方法的源码:

fun <T> mono(
    context: CoroutineContext = DefaultDispatcher,
    parent: Job? = null,
    block: suspend CoroutineScope.() -> T?
): Mono<T> = Mono.create { sink ->
    // 创建一个新的 Coroutine Context
    val newContext = newCoroutineContext(context, parent)
    // 创建一个新的 MonoCoroutine,MonoCoroutine 会实现 Disposable 接口
    val coroutine = MonoCoroutine(newContext, sink)
    sink.onDispose(coroutine)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
}

mono 方法最主要的部分都集中在对 Mono.create 方法的调用。这也是为什么在前面的部分着重介绍 Mono.create 方法和 MonoSink 接口的原因。

Mono.create 会创建出一个 Mono 对象,当这个 Mono 对象的 subscribe 方法被执行的时候。传入 Mono.createConsumer 就会被调用。此时下面的代码就会被执行:

{ sink ->
    // 创建一个新的 Coroutine Context
    val newContext = newCoroutineContext(context, parent)
    // 创建一个新的 MonoCoroutine,MonoCoroutine 会实现 Disposable 接口,用于关闭和异常处理。
    val coroutine = MonoCoroutine(newContext, sink)
    sink.onDispose(coroutine)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
}

上面这段代码就是 Consumer 的 Lambda 形式。

最主要的部分是下面两行代码:

val coroutine = MonoCoroutine(newContext, sink)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)

首先,一个 Coroutine —— MonoCoroutine 被创建。同时,sink 对象被传入到这个 MonoCoroutine 中。

接下来,这个 Coroutine 被执行,接下来的重点变成了 MonoCoroutine

private class MonoCoroutine<in T>(
    parentContext: CoroutineContext,
    private val sink: MonoSink<T>
) : AbstractCoroutine<T>(parentContext, true), Disposable {
    var disposed = false

    override fun onCompleted(value: T) {
        if (!disposed) {
            if (value == null) sink.success() else sink.success(value)
        }
    }

    override fun onCompletedExceptionally(exception: Throwable) {
        if (!disposed) sink.error(exception)
    }
    
    override fun dispose() {
        disposed = true
        cancel(cause = null)
    }

    override fun isDisposed(): Boolean = disposed
}

从上面的代码可以看到,MonoCoroutine 继承了 AbstractCoroutine,同时构造函数入参传入了 MonoSink

MonoCoroutine 实现了两个在 AbstractCoroutine 声明的重要方法:

  • onCompleted
  • onCompletedExceptionally

这两个方法都是回调方法。从上面的代码可以明显看出。当 MonoCoroutine 在执行完毕之后,即这两个回调方法被调用时,会通过调用 MonoSink 将结果输出给 Subscriber。从而完成 Kotlin Coroutine 向 Mono 的转换工作。

2. await 系列方法

接下来要介绍的是一系列以 await 开头的方法,比如示例中的 awaitFirstOrDefaultawaitSingle,等等。这些方法定义在 kotlinx-coroutines-reactive 模块中的 Await.kt 文件。

这些方法都是 suspending 方法,能够用命令式的代码风格获取 PublisherMonoFlux)中的结果。

因为上一篇文章《Kotlin Coroutine 原理解析》已经介绍了 suspending 方法的工作原理,所以这里就不重复了。本文只介绍 Kotlin Coroutine 是如何与 Reactive Streams 中的 Publisher 接口整合的。

await 系列方法可以看作是将 MonoFlux 转换为 Coroutine 的方法。这些方法真正的实现集中在了 awaitOne 方法中。接下来我们看看 awaitOne 方法的源代码:

private suspend fun <T> Publisher<T>.awaitOne(
    mode: Mode,
    default: T? = null
): T = suspendCancellableCoroutine { cont ->
    subscribe(object : Subscriber<T> {
        private lateinit var subscription: Subscription
        private var value: T? = null
        private var seenValue = false

        override fun onSubscribe(sub: Subscription) {
            subscription = sub
            cont.invokeOnCompletion { sub.cancel() }
            sub.request(if (mode == Mode.FIRST) 1 else Long.MAX_VALUE)
        }

        override fun onNext(t: T) {
            when (mode) {
                Mode.FIRST, Mode.FIRST_OR_DEFAULT -> {
                    if (!seenValue) {
                        seenValue = true
                        cont.resume(t)
                        subscription.cancel()
                    }
                }
                Mode.LAST, Mode.SINGLE -> {
                    if (mode == Mode.SINGLE && seenValue) {
                        if (cont.isActive)
                            cont.resumeWithException(IllegalArgumentException("More that one onNext value for $mode"))
                        subscription.cancel()
                    } else {
                        value = t
                        seenValue = true
                    }
                }
            }
        }

        @Suppress("UNCHECKED_CAST")
        override fun onComplete() {
            if (seenValue) {
                if (cont.isActive) cont.resume(value as T)
                return
            }
            when {
                mode == Mode.FIRST_OR_DEFAULT -> {
                    cont.resume(default as T)
                }
                cont.isActive -> {
                    cont.resumeWithException(NoSuchElementException("No value received via onNext for $mode"))
                }
            }
        }

        override fun onError(e: Throwable) {
            cont.resumeWithException(e)
        }
    })
}

逐行解释一下关键代码:

第4行:调用 suspendCancellableCoroutine 方法

这个方法在上一篇文章中解释过了。它是一个特殊的 suspending 方法。不同于普通的 suspending 方法,通过这个方法,开发人员可以获得 Continuation 引用,用来与第三方技术进行集成。

第5行:调用 Publishersubscribe 方法

因为 awaitOne 方法的定义是使用 Kotlin 扩展方法语法添加进了 Publisher 及其所有的子类,所以,可以在 awaitOne 方法中调用 Publisher 中的方法。

同时,调用 subscribe 方法的时候传入一个 Subscriber 接口的匿名内部类。后面大部分代码都是关于如何实现这个匿名内部类的。

在介绍这个 Subscriber 匿名内部类之前,需要先说明,对 subscribe 方法的调用会出发 Publisher 的执行。这也就是说当你调用一个如 aMono.awaitSingle() 方法的时候,就会使 aMono 开始执行。

但是,结合上面一节的内容,这一切的发生,需要当 Spring WebFlux 去调用如本例中 getNumberOfMessages 所返回的 Monosubscribe 方法才会开始。所以,整个执行过程和一个普通的 Spring WebFlux 方法并没有大的区别。

第21行:调用 cont.resume(t)

这行调用发生在 SubscriberonNext 方法中。在反应式编程体系中,Publisher 的每个结果都会通过回调 onNext 方法通知给 Subscriber

onNext 方法里面,当获取到 Publisher 的结果之后,需要将结果传递给 Continuation。方法就是通过 Continuationresume 方法。通过这种方法,Publisher 的结果便传递给了 Kotlin Coroutine。

五、总结

从上面的内容看,Kotlin Coroutine 与 Spring Reactor 的整合的原理并不复杂。主要是实现两个方向的转换:Kotlin Coroutine 向 Mono 的转换和 Mono 向 Kotlin Coroutine 的转换。

Kotlin Coroutine 向 Mono 的转换是通过 Mono.create 方法以及 MonoSink 接口实现的。Kotlin Coroutine 通过 MonoSink 接口,将执行结果输出给 Subscriber

Mono 向 Kotlin Coroutine 的转换是通过使用 suspendCancellableCoroutine 方法获取到 Continuation 引用。再通过调用 Publisher.subscribe 方法,传入一个自定义的 Subscriber。通过 Subscriber.onNext 方法获取到 Publisher 的执行结果,并将这个执行结果传递给 Continuation。从而是 Kotlin Coroutine 获得了 Mono 的执行结果,完成了转换过程。

接下来,Kotlin Coroutine 系列的文章将向大家介绍 JVM 领域其它的协程技术(Quasar Fiber、AliJDK 协程等)与 Kotlin Coroutine 技术的对比。

附:名词解释

为方便大家理解,先向大家介绍本文将会涉及的名词及其含义:

  1. Reactive Streams:一个由多个技术社区共同提出的反应式编程方面的规范
  2. Spring Reactor:Spring 社区提出的反应式编程解决方案,实现 Reactive Streams 规范。
  3. Spring WebFlux:Spring 5 提供了反应式的 Web 开发解决方案,以 Spring Reactor 为基础。用法同 Spring MVC 类似。
  4. Publisher:在本文中指 Reactive Streams 中的一个重要接口。在其它技术中,也被称为 Observable。Publisher 中有一个重要方法 subscribe。Subscriber 可以通过这个方法订阅一个 Publisher,并使 Publisher 开始执行。
  5. Mono/Flux:Spring Reactor 中对 Publisher 接口的实现,分别代表一个元素和多个元素两种场景。
  6. Continuation:异步编程中的一个概念,可以简单理解为 Callback。在 Kotlin Coroutine 中,Continuation 也表示一个具体的回调接口。
  7. Coroutine:协程。在 Kotlin 中,有很多以 Coroutine 命名的类,比如 CoroutineImplAbstractCoroutine。可以简单理解为 Continuation 是一个概念、规范,而 Coroutine 是一种实现机制。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • 适合阅读的人群:本文适合对 Spring、Netty 等框架,以及 Java 8 的 Lambda、Stream ...
    编走编想阅读 49,585评论 39 116
  • 一、前言 Kotlin 是近两年兴起的一门编程语言,最近一年的发展速度很快。在2017年,Google 宣布 Ko...
    编走编想阅读 3,919评论 5 17
  • 上一篇文章《Kotlin Coroutine 初探》向大家介绍了 Kotlin Coroutine 的由来、重要概...
    编走编想阅读 17,540评论 25 50
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • Spring5现处在第四个预发布版,正式版将要发布了,它带来的一大特性就是响应式框架Spring WebFlux。...
    MrTT阅读 26,234评论 0 21