Add support for Coroutines Flow

Flow is a Kotlin Coroutines related cold asynchronous
stream of the data, that emits from zero to N (where N
can be unbounded) values and completes normally or with
an exception.

It is conceptually the Coroutines equivalent of Flux with
an extension oriented API design, easy custom operator
capabilities and some suspending methods.

This commit leverages Flow <-> Flux interoperability
to support Flow on controller handler method parameters
or return values, and also adds Flow based extensions to
WebFlux.fn. It allows to reach a point when we can consider
Spring Framework officially supports Coroutines even if some
additional work remains to be done like adding
interoperability between Reactor and Coroutines contexts.

Flow is currently an experimental API that is expected to
become final before Spring Framework 5.2 GA.

Close gh-19975
This commit is contained in:
Sebastien Deleuze 2019-04-04 17:27:07 +02:00
parent a5e297a161
commit a8d6ba9965
11 changed files with 210 additions and 3 deletions

View File

@ -29,7 +29,7 @@ ext {
} }
aspectjVersion = "1.9.2" aspectjVersion = "1.9.2"
coroutinesVersion = "1.2.0-alpha" coroutinesVersion = "1.2.0-alpha-2"
freemarkerVersion = "2.3.28" freemarkerVersion = "2.3.28"
groovyVersion = "2.5.6" groovyVersion = "2.5.6"
hsqldbVersion = "2.4.1" hsqldbVersion = "2.4.1"

View File

@ -27,6 +27,9 @@ import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable; import io.reactivex.Flowable;
import kotlinx.coroutines.CompletableDeferredKt; import kotlinx.coroutines.CompletableDeferredKt;
import kotlinx.coroutines.Deferred; import kotlinx.coroutines.Deferred;
import kotlinx.coroutines.flow.FlowKt;
import kotlinx.coroutines.reactive.flow.FlowAsPublisherKt;
import kotlinx.coroutines.reactive.flow.PublisherAsFlowKt;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -97,6 +100,10 @@ public class ReactiveAdapterRegistry {
if (ClassUtils.isPresent("kotlinx.coroutines.Deferred", classLoader)) { if (ClassUtils.isPresent("kotlinx.coroutines.Deferred", classLoader)) {
new CoroutinesRegistrar().registerAdapters(this); new CoroutinesRegistrar().registerAdapters(this);
} }
// TODO Use a single CoroutinesRegistrar when Flow will be not experimental anymore
if (ClassUtils.isPresent("kotlinx.coroutines.flow.Flow", classLoader)) {
new CoroutinesFlowRegistrar().registerAdapters(this);
}
} }
@ -335,7 +342,17 @@ public class ReactiveAdapterRegistry {
source -> CoroutinesUtils.deferredToMono((Deferred<?>) source), source -> CoroutinesUtils.deferredToMono((Deferred<?>) source),
source -> CoroutinesUtils.monoToDeferred(Mono.from(source))); source -> CoroutinesUtils.monoToDeferred(Mono.from(source)));
} }
}
private static class CoroutinesFlowRegistrar {
void registerAdapters(ReactiveAdapterRegistry registry) {
registry.registerReactiveType(
ReactiveTypeDescriptor.multiValue(kotlinx.coroutines.flow.Flow.class, FlowKt::emptyFlow),
source -> FlowAsPublisherKt.from((kotlinx.coroutines.flow.Flow<?>) source),
PublisherAsFlowKt::from
);
}
} }
} }

View File

@ -17,14 +17,21 @@
package org.springframework.core package org.springframework.core
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Test import org.junit.Test
import org.reactivestreams.Publisher import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import java.time.Duration import java.time.Duration
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -49,6 +56,36 @@ class KotlinReactiveAdapterRegistryTests {
} }
@Test
@FlowPreview
fun flowToPublisher() {
val source = flow {
emit(1)
emit(2)
emit(3)
}
val target: Publisher<Int> = getAdapter(Flow::class).toPublisher(source)
assertTrue("Expected Flux Publisher: " + target.javaClass.name, target is Flux<*>)
StepVerifier.create(target)
.expectNext(1)
.expectNext(2)
.expectNext(3)
.verifyComplete()
}
@Test
@FlowPreview
fun publisherToFlow() {
val source = Flux.just(1, 2, 3)
val target = getAdapter(Flow::class).fromPublisher(source)
if (target is Flow<*>) {
assertEquals(listOf(1, 2, 3), runBlocking { target.toList() })
}
else {
fail()
}
}
private fun getAdapter(reactiveType: KClass<*>): ReactiveAdapter { private fun getAdapter(reactiveType: KClass<*>): ReactiveAdapter {
return this.registry.getAdapter(reactiveType.java)!! return this.registry.getAdapter(reactiveType.java)!!
} }

View File

@ -16,8 +16,11 @@
package org.springframework.web.reactive.function.client package org.springframework.web.reactive.function.client
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.reactive.awaitFirstOrNull import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactive.awaitSingle import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactive.flow.asFlow
import org.springframework.core.ParameterizedTypeReference import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@ -45,6 +48,19 @@ inline fun <reified T : Any> ClientResponse.bodyToMono(): Mono<T> =
inline fun <reified T : Any> ClientResponse.bodyToFlux(): Flux<T> = inline fun <reified T : Any> ClientResponse.bodyToFlux(): Flux<T> =
bodyToFlux(object : ParameterizedTypeReference<T>() {}) bodyToFlux(object : ParameterizedTypeReference<T>() {})
/**
* Coroutines [kotlinx.coroutines.flow.Flow] based variant of [ClientResponse.bodyToFlux].
*
* Backpressure is controlled by [batchSize] parameter that controls the size of in-flight elements
* and [org.reactivestreams.Subscription.request] size.
*
* @author Sebastien Deleuze
* @since 5.2
*/
@FlowPreview
inline fun <reified T : Any> ClientResponse.bodyToFlow(batchSize: Int = 1): Flow<T> =
bodyToFlux<T>().asFlow(batchSize)
/** /**
* Extension for [ClientResponse.toEntity] providing a `toEntity<Foo>()` variant * Extension for [ClientResponse.toEntity] providing a `toEntity<Foo>()` variant
* leveraging Kotlin reified type parameters. This extension is not subject to type * leveraging Kotlin reified type parameters. This extension is not subject to type

View File

@ -17,8 +17,12 @@
package org.springframework.web.reactive.function.client package org.springframework.web.reactive.function.client
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.reactive.awaitSingle import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactive.flow.asFlow
import kotlinx.coroutines.reactive.flow.asPublisher
import kotlinx.coroutines.reactor.mono import kotlinx.coroutines.reactor.mono
import org.reactivestreams.Publisher import org.reactivestreams.Publisher
import org.springframework.core.ParameterizedTypeReference import org.springframework.core.ParameterizedTypeReference
@ -28,7 +32,7 @@ import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
/** /**
* Extension for [WebClient.RequestBodySpec.body] providing a `body<Foo>()` variant * Extension for [WebClient.RequestBodySpec.body] providing a `body(Publisher<T>)` variant
* leveraging Kotlin reified type parameters. This extension is not subject to type * leveraging Kotlin reified type parameters. This extension is not subject to type
* erasure and retains actual generic type arguments. * erasure and retains actual generic type arguments.
* *
@ -39,6 +43,18 @@ import reactor.core.publisher.Mono
inline fun <reified T : Any, S : Publisher<T>> RequestBodySpec.body(publisher: S): RequestHeadersSpec<*> = inline fun <reified T : Any, S : Publisher<T>> RequestBodySpec.body(publisher: S): RequestHeadersSpec<*> =
body(publisher, object : ParameterizedTypeReference<T>() {}) body(publisher, object : ParameterizedTypeReference<T>() {})
/**
* Coroutines [Flow] based extension for [WebClient.RequestBodySpec.body] providing a
* body(Flow<T>)` variant leveraging Kotlin reified type parameters. This extension is
* not subject to type erasure and retains actual generic type arguments.
*
* @author Sebastien Deleuze
* @since 5.2
*/
@FlowPreview
inline fun <reified T : Any, S : Flow<T>> RequestBodySpec.body(flow: S): RequestHeadersSpec<*> =
body(flow.asPublisher(), object : ParameterizedTypeReference<T>() {})
/** /**
* Extension for [WebClient.ResponseSpec.bodyToMono] providing a `bodyToMono<Foo>()` variant * Extension for [WebClient.ResponseSpec.bodyToMono] providing a `bodyToMono<Foo>()` variant
* leveraging Kotlin reified type parameters. This extension is not subject to type * leveraging Kotlin reified type parameters. This extension is not subject to type
@ -62,6 +78,20 @@ inline fun <reified T : Any> WebClient.ResponseSpec.bodyToMono(): Mono<T> =
inline fun <reified T : Any> WebClient.ResponseSpec.bodyToFlux(): Flux<T> = inline fun <reified T : Any> WebClient.ResponseSpec.bodyToFlux(): Flux<T> =
bodyToFlux(object : ParameterizedTypeReference<T>() {}) bodyToFlux(object : ParameterizedTypeReference<T>() {})
/**
* Coroutines [kotlinx.coroutines.flow.Flow] based variant of [WebClient.ResponseSpec.bodyToFlux].
*
* Backpressure is controlled by [batchSize] parameter that controls the size of in-flight elements
* and [org.reactivestreams.Subscription.request] size.
*
* @author Sebastien Deleuze
* @since 5.2
*/
@FlowPreview
inline fun <reified T : Any> WebClient.ResponseSpec.bodyToFlow(batchSize: Int = 1): Flow<T> =
bodyToFlux<T>().asFlow(batchSize)
/** /**
* Coroutines variant of [WebClient.RequestHeadersSpec.exchange]. * Coroutines variant of [WebClient.RequestHeadersSpec.exchange].
* *

View File

@ -16,8 +16,11 @@
package org.springframework.web.reactive.function.server package org.springframework.web.reactive.function.server
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.reactive.awaitFirstOrNull import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactive.awaitSingle import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactive.flow.asFlow
import org.springframework.core.ParameterizedTypeReference import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.codec.multipart.Part import org.springframework.http.codec.multipart.Part
import org.springframework.util.MultiValueMap import org.springframework.util.MultiValueMap
@ -48,6 +51,19 @@ inline fun <reified T : Any> ServerRequest.bodyToMono(): Mono<T> =
inline fun <reified T : Any> ServerRequest.bodyToFlux(): Flux<T> = inline fun <reified T : Any> ServerRequest.bodyToFlux(): Flux<T> =
bodyToFlux(object : ParameterizedTypeReference<T>() {}) bodyToFlux(object : ParameterizedTypeReference<T>() {})
/**
* Coroutines [kotlinx.coroutines.flow.Flow] based variant of [ServerRequest.bodyToFlux].
*
* Backpressure is controlled by [batchSize] parameter that controls the size of in-flight elements
* and [org.reactivestreams.Subscription.request] size.
*
* @author Sebastien Deleuze
* @since 5.2
*/
@FlowPreview
inline fun <reified T : Any> ServerRequest.bodyToFlow(batchSize: Int = 1): Flow<T> =
bodyToFlux<T>().asFlow(batchSize)
/** /**
* Non-nullable Coroutines variant of [ServerRequest.bodyToMono]. * Non-nullable Coroutines variant of [ServerRequest.bodyToMono].
* *

View File

@ -16,7 +16,10 @@
package org.springframework.web.reactive.function.server package org.springframework.web.reactive.function.server
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.reactive.awaitSingle import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactive.flow.asPublisher
import org.reactivestreams.Publisher import org.reactivestreams.Publisher
import org.springframework.core.ParameterizedTypeReference import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.MediaType import org.springframework.http.MediaType
@ -74,6 +77,19 @@ fun ServerResponse.BodyBuilder.html() = contentType(MediaType.TEXT_HTML)
suspend fun ServerResponse.HeadersBuilder<out ServerResponse.HeadersBuilder<*>>.buildAndAwait(): ServerResponse = suspend fun ServerResponse.HeadersBuilder<out ServerResponse.HeadersBuilder<*>>.buildAndAwait(): ServerResponse =
build().awaitSingle() build().awaitSingle()
/**
* Coroutines [Flow] based extension for [ServerResponse.BodyBuilder.body] providing a
* `body(Flow<T>)` variant. This extension is not subject to type erasure and retains
* actual generic type arguments.
*
* @author Sebastien Deleuze
* @since 5.0
*/
@FlowPreview
suspend inline fun <reified T : Any> ServerResponse.BodyBuilder.bodyAndAwait(flow: Flow<T>): ServerResponse =
body(flow.asPublisher(), object : ParameterizedTypeReference<T>() {}).awaitSingle()
/** /**
* Coroutines variant of [ServerResponse.BodyBuilder.syncBody]. * Coroutines variant of [ServerResponse.BodyBuilder.syncBody].
* *
@ -83,6 +99,18 @@ suspend fun ServerResponse.HeadersBuilder<out ServerResponse.HeadersBuilder<*>>.
suspend fun ServerResponse.BodyBuilder.bodyAndAwait(body: Any): ServerResponse = suspend fun ServerResponse.BodyBuilder.bodyAndAwait(body: Any): ServerResponse =
syncBody(body).awaitSingle() syncBody(body).awaitSingle()
/**
* Coroutines [Flow] based extension for [ServerResponse.BodyBuilder.body] providing a
* `bodyToServerSentEvents(Flow<T>)` variant. This extension is not subject to type
* erasure and retains actual generic type arguments.
*
* @author Sebastien Deleuze
* @since 5.0
*/
@FlowPreview
suspend inline fun <reified T : Any> ServerResponse.BodyBuilder.bodyToServerSentEventsAndAwait(flow: Flow<T>): ServerResponse =
contentType(MediaType.TEXT_EVENT_STREAM).body(flow.asPublisher(), object : ParameterizedTypeReference<T>() {}).awaitSingle()
/** /**
* Coroutines variant of [ServerResponse.BodyBuilder.syncBody] without the sync prefix since it is ok to use it within * Coroutines variant of [ServerResponse.BodyBuilder.syncBody] without the sync prefix since it is ok to use it within

View File

@ -19,6 +19,7 @@ package org.springframework.web.reactive.function.client
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
@ -49,6 +50,13 @@ class ClientResponseExtensionsTests {
verify { response.bodyToFlux(object : ParameterizedTypeReference<List<Foo>>() {}) } verify { response.bodyToFlux(object : ParameterizedTypeReference<List<Foo>>() {}) }
} }
@Test
@FlowPreview
fun `bodyToFlow with reified type parameters`() {
response.bodyToFlow<List<Foo>>()
verify { response.bodyToFlux(object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test @Test
fun `toEntity with reified type parameters`() { fun `toEntity with reified type parameters`() {
response.toEntity<List<Foo>>() response.toEntity<List<Foo>>()

View File

@ -19,6 +19,8 @@ package org.springframework.web.reactive.function.client
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
@ -45,6 +47,14 @@ class WebClientExtensionsTests {
verify { requestBodySpec.body(body, object : ParameterizedTypeReference<List<Foo>>() {}) } verify { requestBodySpec.body(body, object : ParameterizedTypeReference<List<Foo>>() {}) }
} }
@Test
@FlowPreview
fun `RequestBodySpec#body with Flow and reified type parameters`() {
val body = mockk<Flow<List<Foo>>>()
requestBodySpec.body(body)
verify { requestBodySpec.body(ofType<Publisher<List<Foo>>>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test @Test
fun `ResponseSpec#bodyToMono with reified type parameters`() { fun `ResponseSpec#bodyToMono with reified type parameters`() {
responseSpec.bodyToMono<List<Foo>>() responseSpec.bodyToMono<List<Foo>>()
@ -57,6 +67,13 @@ class WebClientExtensionsTests {
verify { responseSpec.bodyToFlux(object : ParameterizedTypeReference<List<Foo>>() {}) } verify { responseSpec.bodyToFlux(object : ParameterizedTypeReference<List<Foo>>() {}) }
} }
@Test
@FlowPreview
fun `bodyToFlow with reified type parameters`() {
responseSpec.bodyToFlow<List<Foo>>()
verify { responseSpec.bodyToFlux(object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test @Test
fun awaitExchange() { fun awaitExchange() {
val response = mockk<ClientResponse>() val response = mockk<ClientResponse>()

View File

@ -19,6 +19,7 @@ package org.springframework.web.reactive.function.client
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
@ -52,6 +53,13 @@ class ServerRequestExtensionsTests {
verify { request.bodyToFlux(object : ParameterizedTypeReference<List<Foo>>() {}) } verify { request.bodyToFlux(object : ParameterizedTypeReference<List<Foo>>() {}) }
} }
@Test
@FlowPreview
fun `bodyToFlow with reified type parameters`() {
request.bodyToFlow<List<Foo>>()
verify { request.bodyToFlux(object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test @Test
fun awaitBody() { fun awaitBody() {
every { request.bodyToMono<String>() } returns Mono.just("foo") every { request.bodyToMono<String>() } returns Mono.just("foo")

View File

@ -19,12 +19,16 @@ package org.springframework.web.reactive.function.server
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.reactivestreams.Publisher import org.reactivestreams.Publisher
import org.springframework.core.ParameterizedTypeReference import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.MediaType
import org.springframework.http.MediaType.* import org.springframework.http.MediaType.*
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
/** /**
@ -48,7 +52,7 @@ class ServerResponseExtensionsTests {
fun `BodyBuilder#bodyToServerSentEvents with Publisher and reified type parameters`() { fun `BodyBuilder#bodyToServerSentEvents with Publisher and reified type parameters`() {
val body = mockk<Publisher<List<Foo>>>() val body = mockk<Publisher<List<Foo>>>()
bodyBuilder.bodyToServerSentEvents(body) bodyBuilder.bodyToServerSentEvents(body)
verify { bodyBuilder.contentType(TEXT_EVENT_STREAM) } verify { bodyBuilder.contentType(TEXT_EVENT_STREAM).body(ofType<Publisher<List<Foo>>>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
} }
@Test @Test
@ -92,6 +96,32 @@ class ServerResponseExtensionsTests {
} }
} }
@Test
@FlowPreview
fun `BodyBuilder#body with Flow and reified type parameters`() {
val response = mockk<ServerResponse>()
val body = mockk<Flow<List<Foo>>>()
every { bodyBuilder.body(ofType<Publisher<List<Foo>>>()) } returns Mono.just(response)
runBlocking {
bodyBuilder.bodyAndAwait(body)
}
verify { bodyBuilder.body(ofType<Publisher<List<Foo>>>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
@FlowPreview
fun `BodyBuilder#bodyToServerSentEvents with Flow and reified type parameters`() {
val response = mockk<ServerResponse>()
val body = mockk<Flow<List<Foo>>>()
every { bodyBuilder.contentType(ofType()) } returns bodyBuilder
every { bodyBuilder.body(ofType<Publisher<List<Foo>>>()) } returns Mono.just(response)
runBlocking {
bodyBuilder.bodyToServerSentEventsAndAwait(body)
}
verify { bodyBuilder.body(ofType<Publisher<List<Foo>>>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
verify { bodyBuilder.contentType(TEXT_EVENT_STREAM) }
}
@Test @Test
fun `renderAndAwait with a vararg parameter`() { fun `renderAndAwait with a vararg parameter`() {
val response = mockk<ServerResponse>() val response = mockk<ServerResponse>()