GH-3902: Add Kotlin Coroutines Support (#3905)

* GH-3902: Add Kotlin Coroutines Support

Fixes https://github.com/spring-projects/spring-integration/issues/3902

* Add `isAsync()` propagation from the `MessagingMethodInvokerHelper`
to the `AbstractMessageProducingHandler` to set into its `async` property.
The logic is based on a `CompletableFuture`, `Publisher` or Kotlin `suspend`
return types of the POJO method
* Introduce `IntegrationMessageHandlerMethodFactory` and `IntegrationInvocableHandlerMethod`
to extend the logic to newly introduced `ContinuationHandlerMethodArgumentResolver`
and call for Kotlin suspend functions.
* Remove `MessageHandlerMethodFactoryCreatingFactoryBean` since its logic now is covered with the
`IntegrationMessageHandlerMethodFactory`
* Kotlin suspend functions are essentially reactive, so use `CoroutinesUtils.invokeSuspendingFunction()`
and existing logic in the `AbstractMessageProducingHandler` to deal with `Publisher` reply

* Fix `GroovySplitterTests` for the current code base

* Add `kotlinx.coroutines.flow.Flow` support
The `Flow` is essentially a multi-value reactive `Publisher`,
so use `ReactiveAdapterRegistry` to convert any custom reactive streams result to `Flux` and `Mono`
which we already support as reply types

* Add docs for `Kotlin Coroutines`
Rearrange the doc a bit extracting Kotlin support to individual `kotlin-functions.adoc` file

* Fix missed link to `reactive-streams.adoc` from the `index-single.adoc`
* Fix unintended Javadocs formatting in the `AbstractMessageProducingHandler`

* Add suspend functions support for Messaging Gateway
* Add convenient `CoroutinesUtils` for Coroutines types and `Continuation` argument fulfilling via `Mono`
* Treat `suspend fun` in the `GatewayProxyFactoryBean` as a `Mono` return
* Convert `Mono` to the `Continuation` resuming in the end of gateway call

* Document `suspend fun` for `@MessagingGateway`

* * Make `async` implicitly only for `suspend fun`

* * Remove unused imports

* * Verify sync and async `Flow` processing
* Mention default sync behavior in the docs

* * Improve reflection in the `CoroutinesUtils`

* Fix language in docs

Co-authored-by: Gary Russell <grussell@vmware.com>

* * Rebase and revert blank lines around `include` in docs

Co-authored-by: Gary Russell <grussell@vmware.com>
This commit is contained in:
Artem Bilan
2022-10-17 17:53:55 -04:00
committed by GitHub
parent cfeaecaae8
commit ff076b6a19
22 changed files with 612 additions and 231 deletions

View File

@@ -17,27 +17,23 @@
package org.springframework.integration.function
import assertk.assertThat
import assertk.assertions.containsAll
import assertk.assertions.isEqualTo
import assertk.assertions.isNotNull
import assertk.assertions.isTrue
import assertk.assertions.size
import assertk.assertions.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.integration.annotation.EndpointId
import org.springframework.integration.annotation.InboundChannelAdapter
import org.springframework.integration.annotation.Poller
import org.springframework.integration.annotation.ServiceActivator
import org.springframework.integration.annotation.Transformer
import org.springframework.integration.annotation.*
import org.springframework.integration.channel.DirectChannel
import org.springframework.integration.channel.FluxMessageChannel
import org.springframework.integration.channel.QueueChannel
import org.springframework.integration.config.EnableIntegration
import org.springframework.integration.dsl.integrationFlow
import org.springframework.integration.endpoint.SourcePollingChannelAdapter
import org.springframework.integration.gateway.GatewayProxyFactoryBean
import org.springframework.integration.handler.ServiceActivatingHandler
import org.springframework.messaging.Message
import org.springframework.messaging.MessageChannel
import org.springframework.messaging.PollableChannel
@@ -46,8 +42,10 @@ import org.springframework.messaging.support.GenericMessage
import org.springframework.messaging.support.MessageBuilder
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import java.time.Duration
import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@@ -88,8 +86,8 @@ class FunctionsTests {
val replyChannel = QueueChannel()
val message = MessageBuilder.withPayload("foo")
.setReplyChannel(replyChannel)
.build()
.setReplyChannel(replyChannel)
.build()
this.functionServiceChannel.send(message)
@@ -98,8 +96,8 @@ class FunctionsTests {
val payload = receive?.payload
assertThat(payload)
.isNotNull()
.isEqualTo("FOO")
.isNotNull()
.isEqualTo("FOO")
}
@Test
@@ -139,8 +137,8 @@ class FunctionsTests {
val mono = this.monoFunction.apply("test")
StepVerifier.create(mono.map(Message<*>::getPayload).cast(String::class.java))
.expectNext("TEST")
.verifyComplete()
.expectNext("TEST")
.verifyComplete()
val gateways = this.monoFunctionGateway.gateways
assertThat(gateways).size().isEqualTo(3)
@@ -148,8 +146,97 @@ class FunctionsTests {
assertThat(methodNames).containsAll("apply", "andThen", "compose")
}
@Autowired
private lateinit var suspendServiceChannel: MessageChannel
@Test
fun `verify suspend function`() {
val replyChannel = FluxMessageChannel()
val testPayload = "test coroutine"
val stepVerifier =
StepVerifier.create(Flux.from(replyChannel).map(Message<*>::getPayload).cast(String::class.java))
.expectNext(testPayload.uppercase())
.thenCancel()
.verifyLater()
suspendServiceChannel.send(
MessageBuilder.withPayload(testPayload)
.setReplyChannel(replyChannel)
.build()
)
stepVerifier.verify(Duration.ofSeconds(10))
}
@Autowired
private lateinit var flowServiceChannel: MessageChannel
@Test
fun `verify flow function`() {
val replyChannel = FluxMessageChannel()
val testPayload = "test flow"
val stepVerifier =
StepVerifier.create(Flux.from(replyChannel).map(Message<*>::getPayload).cast(String::class.java))
.expectNext("$testPayload #1", "$testPayload #2", "$testPayload #3")
.thenCancel()
.verifyLater()
flowServiceChannel.send(
MessageBuilder.withPayload(testPayload)
.setReplyChannel(replyChannel)
.build()
)
stepVerifier.verify(Duration.ofSeconds(10))
}
@Autowired
private lateinit var syncFlowServiceChannel: MessageChannel
@Test
fun `verify sync flow function reply`() {
val replyChannel = QueueChannel()
val testPayload = "test flow"
syncFlowServiceChannel.send(
MessageBuilder.withPayload(testPayload)
.setReplyChannel(replyChannel)
.build()
)
val receive = replyChannel.receive(10_000)
val payload = receive?.payload
assertThat(payload)
.isNotNull()
.isInstanceOf(Flow::class)
runBlocking {
val strings = (payload as Flow<String>).toList()
assertThat(strings).containsExactly("Sync $testPayload #1", "Sync $testPayload #2", "Sync $testPayload #3")
}
}
@Autowired
private lateinit var suspendRequestChannel: DirectChannel
@Autowired
private lateinit var suspendFunGateway: SuspendFunGateway
@Test
fun `suspend gateway`() {
suspendRequestChannel.subscribe(ServiceActivatingHandler { m -> m.payload.toString().uppercase() })
runBlocking {
val reply = suspendFunGateway.suspendGateway("test suspend gateway")
assertThat(reply).isEqualTo("TEST SUSPEND GATEWAY")
}
}
@Configuration
@EnableIntegration
@IntegrationComponentScan
class Config {
@Bean
@@ -171,8 +258,10 @@ class FunctionsTests {
fun counterChannel() = DirectChannel()
@Bean
@InboundChannelAdapter(value = "counterChannel", autoStartup = "false",
poller = Poller(fixedRate = "10", maxMessagesPerPoll = "1"))
@InboundChannelAdapter(
value = "counterChannel", autoStartup = "false",
poller = Poller(fixedRate = "10", maxMessagesPerPoll = "1")
)
@EndpointId("kotlinSupplierChannelAdapter")
fun kotlinSupplier(): () -> String {
return { "baz" }
@@ -180,16 +269,43 @@ class FunctionsTests {
@Bean
fun flowFromSupplier() =
integrationFlow({ "" }, { poller { it.fixedDelay(10).maxMessagesPerPoll(1) } }) {
transform<String> { "blank" }
channel { queue("fromSupplierQueue") }
}
integrationFlow({ "" }, { poller { it.fixedDelay(10).maxMessagesPerPoll(1) } }) {
transform<String> { "blank" }
channel { queue("fromSupplierQueue") }
}
@Bean
fun monoFunctionGateway() =
integrationFlow<MonoFunction>({ proxyDefaultMethods(true) }) {
handle<String>({ p, _ -> Mono.just(p).map(String::uppercase) }) { async(true) }
integrationFlow<MonoFunction>({ proxyDefaultMethods(true) }) {
handle<String>({ p, _ -> Mono.just(p).map(String::uppercase) }) { async(true) }
}
@ServiceActivator(inputChannel = "suspendServiceChannel")
suspend fun suspendServiceFunction(payload: String) = payload.uppercase()
@ServiceActivator(inputChannel = "flowServiceChannel", async = "true")
fun flowServiceFunction(payload: String) =
flow {
for (i in 1..3) {
emit("$payload #$i")
}
}
@ServiceActivator(inputChannel = "syncFlowServiceChannel")
fun syncFlowServiceFunction(payload: String) =
(1..3).asFlow()
.map { "Sync $payload #$it" }
@Bean
fun suspendRequestChannel() = DirectChannel()
}
@MessagingGateway(defaultRequestChannel = "suspendRequestChannel")
interface SuspendFunGateway {
suspend fun suspendGateway(payload: String): String
}