From 10b478d92e7982641e18ae898f1a22d119a50c39 Mon Sep 17 00:00:00 2001 From: Glenn Renfro Date: Thu, 20 Nov 2025 07:56:58 -0500 Subject: [PATCH 1/6] Add Kotlin Hello World sample for Spring Integration This commit introduces a Kotlin-based implementation of the Hello World sample, demonstrating Spring Integration's Kotlin DSL capabilities. The sample includes two applications: a basic Hello World flow and a Poller application. The implementation consists of: - HelloService and HelloWorldConfig using Kotlin DSL for message flows - PollerConfig demonstrating time-based polling with Kotlin syntax - Test suite with unit and integration tests - Maven POM with Kotlin plugin configuration - README with setup instructions and code examples --- basic/helloworld-kotlin/README.md | 38 +++ basic/helloworld-kotlin/pom.xml | 231 ++++++++++++++++++ .../samples/helloworld/HelloService.kt | 28 +++ .../samples/helloworld/HelloWorldApp.kt | 45 ++++ .../samples/helloworld/HelloWorldConfig.kt | 53 ++++ .../samples/helloworld/PollerApp.kt | 38 +++ .../samples/helloworld/PollerConfig.kt | 44 ++++ .../src/main/resources/log4j2.xml | 14 ++ .../samples/helloworld/HelloServiceTests.kt | 36 +++ .../helloworld/HelloWorldConfigTests.kt | 63 +++++ .../samples/helloworld/PollerConfigTests.kt | 93 +++++++ build.gradle | 37 +++ 12 files changed, 720 insertions(+) create mode 100644 basic/helloworld-kotlin/README.md create mode 100644 basic/helloworld-kotlin/pom.xml create mode 100644 basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloService.kt create mode 100644 basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldApp.kt create mode 100644 basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfig.kt create mode 100644 basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerApp.kt create mode 100644 basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerConfig.kt create mode 100644 basic/helloworld-kotlin/src/main/resources/log4j2.xml create mode 100644 basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/HelloServiceTests.kt create mode 100644 basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfigTests.kt create mode 100644 basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/PollerConfigTests.kt diff --git a/basic/helloworld-kotlin/README.md b/basic/helloworld-kotlin/README.md new file mode 100644 index 000000000..ef6a70a9c --- /dev/null +++ b/basic/helloworld-kotlin/README.md @@ -0,0 +1,38 @@ +Hello World Sample +================== + +This is the Kotlin version of the helloworld Java sample using Kotlin DSL. This sample project contains 2 basic sample applications: + +* Hello World +* Poller Application + +## Hello World + +The Hello World application demonstrates a simple message flow represented by the diagram below: + + Message -> Channel -> ServiceActivator -> QueueChannel + +To run the sample simply execute **HelloWorldApp** in package **org.springframework.integration.samples.helloworld**. +You can also execute that class using the [Gradle](https://www.gradle.org): + + $ gradlew :helloworld-kotlin:runHelloWorldApp + +You should see the following output: + + INFO : org.springframework.integration.samples.helloworld.HelloWorldApp - ==> HelloWorldDemo: Hello World + +## Poller Application + +This simple application will print out the current system time twice every 20 seconds. + +More specifically, an **Inbound Channel Adapter** polls for the current system time 2 times every 20 seconds (20000 milliseconds). The resulting message contains as payload the time in milliseconds and the message is sent to a **Logging Channel Adapter**, which will print the time to the command prompt. + +To run the sample simply execute **PollerApp** in package **org.springframework.integration.samples.helloworld**. +You can also execute that class using the [Gradle](https://www.gradle.org): + + $ gradlew :helloworld-kotlin:runPollerApp + +You should see output like the following: + +[task-scheduler-1][org.springframework.integration.samples.helloworld] GenericMessage [payload=1763478785243, headers={id=8f93b18a-063a-5e9f-4708-2ed1d04a1566, timestamp=1763478785244}] +[task-scheduler-1][org.springframework.integration.samples.helloworld] GenericMessage [payload=1763478785248, headers={id=aa37e9c4-95d1-538c-a6cd-d400bb1474bf, timestamp=1763478785248}] diff --git a/basic/helloworld-kotlin/pom.xml b/basic/helloworld-kotlin/pom.xml new file mode 100644 index 000000000..dfa8107ad --- /dev/null +++ b/basic/helloworld-kotlin/pom.xml @@ -0,0 +1,231 @@ + + + 4.0.0 + org.springframework.integration.samples + helloworld-kotlin + 7.0.0 + https://github.com/spring-projects/spring-integration-samples + + Spring IO + https://spring.io/projects/spring-integration + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + artembilan + Artem Bilan + artem.bilan@broadcom.com + + project lead + + + + garyrussell + Gary Russell + github@gprussell.net + + project lead emeritus + + + + markfisher + Mark Fisher + mark.ryan.fisher@gmail.com + + project founder and lead emeritus + + + + cppwfs + Glenn Renfro + glenn.renfro@broadcom.com + + project committer + + + + + scm:git:scm:git:git://github.com/spring-projects/spring-integration-samples.git + scm:git:scm:git:ssh://git@github.com:spring-projects/spring-integration-samples.git + https://github.com/spring-projects/spring-integration-samples + + + GitHub + https://github.com/spring-projects/spring-integration-samples/issues + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + org.junit + junit-bom + 6.0.0 + import + pom + + + tools.jackson + jackson-bom + 3.0.0-rc9 + import + pom + + + com.fasterxml.jackson + jackson-bom + 2.20.0 + import + pom + + + org.springframework + spring-framework-bom + 7.0.0-SNAPSHOT + import + pom + + + org.springframework.integration + spring-integration-bom + 7.0.0-SNAPSHOT + import + pom + + + + + + org.apache.logging.log4j + log4j-core + 2.24.3 + compile + + + org.springframework.integration + spring-integration-core + compile + + + org.jetbrains.kotlin + kotlin-stdlib + compile + + + org.jetbrains.kotlin + kotlin-reflect + compile + + + org.hamcrest + hamcrest-library + 2.2 + test + + + org.mockito + mockito-core + 5.18.0 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.springframework.integration + spring-integration-test + test + + + org.apache.logging.log4j + log4j-core-test + 2.24.3 + test + + + org.awaitility + awaitility + 4.2.2 + test + + + org.assertj + assertj-core + 3.27.6 + test + + + org.junit.jupiter + junit-jupiter-engine + runtime + + + org.junit.platform + junit-platform-launcher + runtime + + + + 17 + 2.2.21 + + + + repo.spring.io.milestone + Spring Framework Maven Milestone Repository + https://repo.spring.io/milestone + + + repo.spring.io.snapshot + Spring Framework Maven Snapshot Repository + https://repo.spring.io/snapshot + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + 17 + + -Xjdk-release=17 + + + + + + diff --git a/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloService.kt b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloService.kt new file mode 100644 index 000000000..d766202d9 --- /dev/null +++ b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloService.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.samples.helloworld + +/** + * Simple POJO to be referenced from a Service Activator. + * + * @author Glenn Renfro + */ +class HelloService { + + fun sayHello(name: String) = "Hello $name" + +} diff --git a/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldApp.kt b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldApp.kt new file mode 100644 index 000000000..7a9c78d0f --- /dev/null +++ b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldApp.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.samples.helloworld + +import org.apache.commons.logging.LogFactory +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.messaging.MessageChannel +import org.springframework.messaging.PollableChannel +import org.springframework.messaging.support.GenericMessage + +/** + * Demonstrates a basic Message Endpoint that simply prepends a greeting + * ("Hello ") to an inbound String payload from a Message. + * + * @author Glenn Renfro + */ +object HelloWorldApp { + + private val logger = LogFactory.getLog(HelloWorldApp::class.java) + + @JvmStatic + fun main(args: Array) { + val context = AnnotationConfigApplicationContext(HelloWorldConfig::class.java) + val inputChannel = context.getBean("inputChannel", MessageChannel::class.java) + val outputChannel = context.getBean("outputChannel", PollableChannel::class.java) + inputChannel.send(GenericMessage("World")) + logger.info("==> HelloWorldDemo: ${outputChannel.receive(0)?.payload}") + context.close() + } + +} diff --git a/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfig.kt b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfig.kt new file mode 100644 index 000000000..e65b6fea9 --- /dev/null +++ b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfig.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.samples.helloworld + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.integration.channel.DirectChannel +import org.springframework.integration.channel.QueueChannel +import org.springframework.integration.config.EnableIntegration +import org.springframework.integration.dsl.IntegrationFlow +import org.springframework.integration.dsl.integrationFlow +import org.springframework.messaging.MessageChannel + +/** + * Configuration for the HelloWorld integration flow using Kotlin DSL. + * + * @author Glenn Renfro + */ +@Configuration +@EnableIntegration +open class HelloWorldConfig { + + @Bean + open fun inputChannel() = DirectChannel() + + @Bean + open fun outputChannel() = QueueChannel(10) + + @Bean + open fun helloService() = HelloService() + + @Bean + open fun helloWorldFlow(inputChannel: MessageChannel, + outputChannel: MessageChannel) = integrationFlow(inputChannel) { + handle(helloService(), "sayHello") + channel(outputChannel) + } + +} diff --git a/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerApp.kt b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerApp.kt new file mode 100644 index 000000000..11980cbbf --- /dev/null +++ b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerApp.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.samples.helloworld + +import org.springframework.context.annotation.AnnotationConfigApplicationContext + +/** + * Simple application that polls the current system time 2 times every + * 20 seconds (20000 milliseconds). + * + * The resulting message contains the time in milliseconds and the message + * is routed to a Logging Channel Adapter which will print the time to the + * command prompt. + * + * @author Glenn Renfro + */ +object PollerApp { + + @JvmStatic + fun main(args: Array) { + AnnotationConfigApplicationContext(PollerConfig::class.java) + } + +} diff --git a/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerConfig.kt b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerConfig.kt new file mode 100644 index 000000000..e773a1d44 --- /dev/null +++ b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerConfig.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.samples.helloworld + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.integration.config.EnableIntegration +import org.springframework.integration.dsl.IntegrationFlow +import org.springframework.integration.handler.LoggingHandler + +/** + * Configuration for the Poller integration flow using the Kotlin DSL. + * This flow polls for the current system time every 20 seconds and logs it. + * + * @author Glenn Renfro + */ +@Configuration +@EnableIntegration +open class PollerConfig { + + @Bean + open fun pollerFlow() = + IntegrationFlow.fromSupplier({ System.currentTimeMillis() }) { e -> + e.poller { p -> + p.fixedDelay(20000).maxMessagesPerPoll(2) + } + } + .log(LoggingHandler.Level.INFO, "org.springframework.integration.samples.helloworld") + .nullChannel() +} diff --git a/basic/helloworld-kotlin/src/main/resources/log4j2.xml b/basic/helloworld-kotlin/src/main/resources/log4j2.xml new file mode 100644 index 000000000..9cb8aea38 --- /dev/null +++ b/basic/helloworld-kotlin/src/main/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/HelloServiceTests.kt b/basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/HelloServiceTests.kt new file mode 100644 index 000000000..6b0ab5fd3 --- /dev/null +++ b/basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/HelloServiceTests.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.samples.helloworld + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +/** + * Unit tests for HelloService. + * + * @author Glenn Renfro + */ +class HelloServiceTests { + + @Test + fun testSayHello() { + val service = HelloService() + val result = service.sayHello("World") + assertThat(result).isEqualTo("Hello World") + } + +} diff --git a/basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfigTests.kt b/basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfigTests.kt new file mode 100644 index 000000000..df5acb516 --- /dev/null +++ b/basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfigTests.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.samples.helloworld + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.messaging.MessageChannel +import org.springframework.messaging.PollableChannel +import org.springframework.messaging.support.GenericMessage +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig + +/** + * Integration tests for HelloWorld flow. + * + * @author Glenn Renfro + */ +@SpringJUnitConfig(HelloWorldConfig::class) +class HelloWorldConfigTests { + + @Autowired + lateinit var inputChannel: MessageChannel + + @Autowired + lateinit var outputChannel: PollableChannel + + @Test + fun testHelloWorldFlow() { + inputChannel.send(GenericMessage("World")) + val message = outputChannel.receive(1000) + assertThat(message).isNotNull + assertThat(message?.payload).isNotNull + } + + @Test + fun testMultipleMessages() { + inputChannel.send(GenericMessage("Test1")) + inputChannel.send(GenericMessage("Test2")) + + val message1 = outputChannel.receive(1000) + assertThat(message1).isNotNull + assertThat(message1?.payload).isEqualTo("Hello Test1") + + val message2 = outputChannel.receive(1000) + assertThat(message2).isNotNull + assertThat(message2?.payload).isEqualTo("Hello Test2") + } + +} diff --git a/basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/PollerConfigTests.kt b/basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/PollerConfigTests.kt new file mode 100644 index 000000000..ae4ce636c --- /dev/null +++ b/basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/PollerConfigTests.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.samples.helloworld + +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.core.LoggerContext +import org.apache.logging.log4j.core.test.appender.ListAppender +import org.assertj.core.api.Assertions.assertThat +import org.awaitility.Awaitility.await +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.integration.dsl.IntegrationFlow +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig +import java.time.Duration + + +/** + * Integration tests for Poller flow. + * Tests verify that the poller actually polls and produces messages. + * + * @author Glenn Renfro + */ +@SpringJUnitConfig(PollerConfig::class) +class PollerConfigTests { + + @Autowired + lateinit var pollerFlow: IntegrationFlow + + companion object { + lateinit var listAppender: ListAppender + + @JvmStatic + @BeforeAll + fun setupLogger() { + val loggerContext = LogManager.getContext(false) as LoggerContext + listAppender = ListAppender("TestAppender") + listAppender.start() + loggerContext.configuration.addAppender(listAppender) + loggerContext.rootLogger.addAppender(listAppender) + loggerContext.updateLoggers() + } + + @JvmStatic + @AfterAll + fun cleanupLogger() { + val loggerContext = LogManager.getContext(false) as LoggerContext + loggerContext.rootLogger.removeAppender(listAppender) + listAppender.stop() + } + } + + @Test + fun testPollerFlowBeanExists() { + assertThat(pollerFlow).isNotNull + } + + @Test + fun testPollerFlowConfiguration() { + val integrationComponents = pollerFlow.integrationComponents + assertThat(integrationComponents).isNotNull + assertThat(integrationComponents.size).isGreaterThan(0) + } + + @Test + fun testPollerIsActiveAndRunning() { + await() + .atMost(Duration.ofSeconds(5)) + .until { listAppender.events.isNotEmpty() } + + assertThat(listAppender.events) + .anyMatch { event -> + event.toString() + .contains("org.springframework.integration.samples.helloworld Level=INFO " + + "Message=GenericMessage [payload=")} + + } +} diff --git a/build.gradle b/build.gradle index b48103284..e8160d3fe 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ buildscript { classpath 'io.spring.gradle:dependency-management-plugin:1.1.7' classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" classpath 'org.gretty:gretty:4.1.10' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21' } } @@ -212,6 +213,13 @@ subprojects { subproject -> targetCompatibility = JavaVersion.VERSION_17 } + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + jvmTarget = '17' + } + } + + compileTestJava { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -221,6 +229,8 @@ subprojects { subproject -> ext { artemisVersion = '2.41.0' aspectjVersion = '1.9.24' + assertjVersion = '3.27.6' + awaitilityVersion = '4.2.2' commonsDigesterVersion = '2.1' commonsDbcpVersion = '2.13.0' commonsFileUploadVersion = '1.6.0' @@ -609,8 +619,35 @@ project('helloworld-groovy') { api "org.apache.logging.log4j:log4j-core:$log4jVersion" api "org.springframework.integration:spring-integration-groovy" testImplementation "org.apache.logging.log4j:log4j-core-test:$log4jVersion" + testImplementation "org.awaitility:awaitility:$awaitilityVersion" + testImplementation("org.assertj:assertj-core:$assertjVersion") } + + tasks.register ('runHelloWorldApp', JavaExec) { + mainClass = 'org.springframework.integration.samples.helloworld.HelloWorldApp' + classpath = sourceSets.main.runtimeClasspath } + tasks.register ('runPollerApp', JavaExec) { + mainClass = 'org.springframework.integration.samples.helloworld.PollerApp' + classpath = sourceSets.main.runtimeClasspath + } + +} + +project('helloworld-kotlin') { + description = 'Hello World Sample for Kotlin Developers' + + apply plugin: 'org.jetbrains.kotlin.jvm' + + dependencies { + api "org.apache.logging.log4j:log4j-core:$log4jVersion" + api "org.springframework.integration:spring-integration-core" + api "org.jetbrains.kotlin:kotlin-stdlib" + api "org.jetbrains.kotlin:kotlin-reflect" + testImplementation "org.apache.logging.log4j:log4j-core-test:$log4jVersion" + testImplementation "org.awaitility:awaitility:$awaitilityVersion" + testImplementation("org.assertj:assertj-core:$assertjVersion") } + tasks.register ('runHelloWorldApp', JavaExec) { mainClass = 'org.springframework.integration.samples.helloworld.HelloWorldApp' classpath = sourceSets.main.runtimeClasspath From af71c1ef977e4d23755c85c0f1db9eef5e6294ca Mon Sep 17 00:00:00 2001 From: Glenn Renfro Date: Thu, 20 Nov 2025 11:27:59 -0500 Subject: [PATCH 2/6] Add documentation to configurations Revise integrationFlow to be idiomatic of the Kotlin DSL --- .../samples/helloworld/HelloWorldConfig.kt | 44 ++++++++++++++++--- .../samples/helloworld/PollerConfig.kt | 27 ++++++++---- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfig.kt b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfig.kt index e65b6fea9..083e12c1d 100644 --- a/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfig.kt +++ b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfig.kt @@ -21,7 +21,6 @@ import org.springframework.context.annotation.Configuration import org.springframework.integration.channel.DirectChannel import org.springframework.integration.channel.QueueChannel import org.springframework.integration.config.EnableIntegration -import org.springframework.integration.dsl.IntegrationFlow import org.springframework.integration.dsl.integrationFlow import org.springframework.messaging.MessageChannel @@ -30,24 +29,57 @@ import org.springframework.messaging.MessageChannel * * @author Glenn Renfro */ -@Configuration +@Configuration(proxyBeanMethods = false) @EnableIntegration open class HelloWorldConfig { + /** + * Creates the input channel for inbound messages. + * + * A [DirectChannel] is used for synchronous, immediate message delivery. + * Messages arriving on this channel are processed on the sender's thread + * without any buffering or queuing. + * + * @return A [DirectChannel] instance for synchronous inbound message delivery + */ @Bean open fun inputChannel() = DirectChannel() + /** + * Creates the output channel for outbound messages. + * + * A [QueueChannel] with capacity of 10 messages provides asynchronous, + * buffered message delivery. Results from the integration flow are queued + * and available for downstream consumption. + * + * @return A [QueueChannel] instance with capacity of 10 messages + */ @Bean open fun outputChannel() = QueueChannel(10) + /** + * Creates the Hello World business service. + * + * [HelloService] implements the core greeting logic that transforms + * input messages into personalized greeting responses. + * + * @return A [HelloService] instance + */ @Bean open fun helloService() = HelloService() + /** + * Defines the main integration flow for message processing. + * + * @param inputChannel The synchronous input channel receiving messages + * @param outputChannel The asynchronous output channel for results + * @param helloService The service implementing the greeting logic + * @return An IntegrationFlow representing the complete message flow + */ @Bean - open fun helloWorldFlow(inputChannel: MessageChannel, - outputChannel: MessageChannel) = integrationFlow(inputChannel) { - handle(helloService(), "sayHello") + open fun helloWorldFlow(inputChannel: MessageChannel, outputChannel: MessageChannel, helloService: HelloService) = + integrationFlow(inputChannel) { + handle(helloService, "sayHello") channel(outputChannel) } - } diff --git a/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerConfig.kt b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerConfig.kt index e773a1d44..899572452 100644 --- a/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerConfig.kt +++ b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerConfig.kt @@ -18,8 +18,10 @@ package org.springframework.integration.samples.helloworld import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.integration.channel.NullChannel import org.springframework.integration.config.EnableIntegration -import org.springframework.integration.dsl.IntegrationFlow +import org.springframework.integration.dsl.integrationFlow +import org.springframework.integration.endpoint.MessageProcessorMessageSource import org.springframework.integration.handler.LoggingHandler /** @@ -28,17 +30,26 @@ import org.springframework.integration.handler.LoggingHandler * * @author Glenn Renfro */ -@Configuration +@Configuration(proxyBeanMethods = false) @EnableIntegration open class PollerConfig { + /** + * Defines a polling-based integration flow for periodic message generation. + * + * This flow demonstrates a time-triggered, scheduled message processing pattern. + * A message source generates timestamps at fixed intervals, logs the values, + * and discards them via the null channel. This is useful for monitoring, + * health checks, or triggering periodic processing logic. + * + * @return An IntegrationFlow that periodically generates and logs timestamps, then discards them. + */ @Bean open fun pollerFlow() = - IntegrationFlow.fromSupplier({ System.currentTimeMillis() }) { e -> - e.poller { p -> - p.fixedDelay(20000).maxMessagesPerPoll(2) + integrationFlow( + MessageProcessorMessageSource { System.currentTimeMillis() }, + { poller { it.fixedDelay(20000).maxMessagesPerPoll(2) } }) { + log(LoggingHandler.Level.INFO, "org.springframework.integration.samples.helloworld") + channel(NullChannel()) } - } - .log(LoggingHandler.Level.INFO, "org.springframework.integration.samples.helloworld") - .nullChannel() } From fc0597a2b6af08c0a5fb3131cef2cda616faf9a7 Mon Sep 17 00:00:00 2001 From: Glenn Renfro Date: Thu, 20 Nov 2025 13:50:03 -0500 Subject: [PATCH 3/6] Configure Kotlin Spring plugin for proper proxy support Update both Maven and Gradle build configurations to properly support Kotlin with Spring Framework. The Kotlin Spring plugin automatically handles making configuration classes open for proxying, eliminating the need for manual 'open' modifiers. Update maven generation scripts to handle kotlin plugin. Add execution compile section so classes are included in the jar. This can be tested via mvn clean package or to run mvn exec:java -Dexec.mainClass="org.springframework.integration.samples.helloworld.PollerApp" Changes include: - Add kotlin-spring and kotlin-allopen plugins to build configurations - Remove unnecessary 'open' modifiers from @Configuration classes - Configure Kotlin Maven plugin with Spring compiler plugin - Simplify QueueChannel initialization to use default capacity - Update MessageProcessorMessageSource to use simpler lambda syntax - Adjust logging configuration to reduce noise from Spring framework --- .../helloworld/HelloWorldConfig.groovy | 2 +- basic/helloworld-kotlin/pom.xml | 30 ++++++---- .../samples/helloworld/HelloWorldConfig.kt | 14 ++--- .../samples/helloworld/PollerConfig.kt | 7 +-- .../src/main/resources/log4j2.xml | 5 +- build.gradle | 60 ++++++++++++++++++- 6 files changed, 91 insertions(+), 27 deletions(-) diff --git a/basic/helloworld-groovy/src/main/groovy/org/springframework/integration/samples/helloworld/HelloWorldConfig.groovy b/basic/helloworld-groovy/src/main/groovy/org/springframework/integration/samples/helloworld/HelloWorldConfig.groovy index f572d18ed..f186de378 100644 --- a/basic/helloworld-groovy/src/main/groovy/org/springframework/integration/samples/helloworld/HelloWorldConfig.groovy +++ b/basic/helloworld-groovy/src/main/groovy/org/springframework/integration/samples/helloworld/HelloWorldConfig.groovy @@ -43,7 +43,7 @@ class HelloWorldConfig { @Bean MessageChannel outputChannel() { - new QueueChannel(10) + new QueueChannel() } @Bean diff --git a/basic/helloworld-kotlin/pom.xml b/basic/helloworld-kotlin/pom.xml index dfa8107ad..2e8a454e7 100644 --- a/basic/helloworld-kotlin/pom.xml +++ b/basic/helloworld-kotlin/pom.xml @@ -64,12 +64,12 @@ org.jetbrains.kotlin kotlin-stdlib - ${kotlin.version} + 2.2.21 org.jetbrains.kotlin kotlin-reflect - ${kotlin.version} + 2.2.21 org.junit @@ -183,7 +183,6 @@ 17 - 2.2.21 @@ -198,11 +197,21 @@ + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin org.jetbrains.kotlin kotlin-maven-plugin - ${kotlin.version} + 2.2.21 + + + -Xjsr305=strict + + + spring + + compile @@ -219,12 +228,13 @@ - - 17 - - -Xjdk-release=17 - - + + + org.jetbrains.kotlin + kotlin-maven-allopen + 2.2.21 + + diff --git a/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfig.kt b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfig.kt index 083e12c1d..bbe2c5efa 100644 --- a/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfig.kt +++ b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/HelloWorldConfig.kt @@ -31,7 +31,7 @@ import org.springframework.messaging.MessageChannel */ @Configuration(proxyBeanMethods = false) @EnableIntegration -open class HelloWorldConfig { +class HelloWorldConfig { /** * Creates the input channel for inbound messages. @@ -43,19 +43,19 @@ open class HelloWorldConfig { * @return A [DirectChannel] instance for synchronous inbound message delivery */ @Bean - open fun inputChannel() = DirectChannel() + fun inputChannel() = DirectChannel() /** * Creates the output channel for outbound messages. * - * A [QueueChannel] with capacity of 10 messages provides asynchronous, + * A [QueueChannel] with default capacity provides asynchronous, * buffered message delivery. Results from the integration flow are queued * and available for downstream consumption. * - * @return A [QueueChannel] instance with capacity of 10 messages + * @return A [QueueChannel] instance with default capacity. */ @Bean - open fun outputChannel() = QueueChannel(10) + fun outputChannel() = QueueChannel() /** * Creates the Hello World business service. @@ -66,7 +66,7 @@ open class HelloWorldConfig { * @return A [HelloService] instance */ @Bean - open fun helloService() = HelloService() + fun helloService() = HelloService() /** * Defines the main integration flow for message processing. @@ -77,7 +77,7 @@ open class HelloWorldConfig { * @return An IntegrationFlow representing the complete message flow */ @Bean - open fun helloWorldFlow(inputChannel: MessageChannel, outputChannel: MessageChannel, helloService: HelloService) = + fun helloWorldFlow(inputChannel: MessageChannel, outputChannel: MessageChannel, helloService: HelloService) = integrationFlow(inputChannel) { handle(helloService, "sayHello") channel(outputChannel) diff --git a/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerConfig.kt b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerConfig.kt index 899572452..1e136dec2 100644 --- a/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerConfig.kt +++ b/basic/helloworld-kotlin/src/main/kotlin/org/springframework/integration/samples/helloworld/PollerConfig.kt @@ -21,7 +21,6 @@ import org.springframework.context.annotation.Configuration import org.springframework.integration.channel.NullChannel import org.springframework.integration.config.EnableIntegration import org.springframework.integration.dsl.integrationFlow -import org.springframework.integration.endpoint.MessageProcessorMessageSource import org.springframework.integration.handler.LoggingHandler /** @@ -32,7 +31,7 @@ import org.springframework.integration.handler.LoggingHandler */ @Configuration(proxyBeanMethods = false) @EnableIntegration -open class PollerConfig { +class PollerConfig { /** * Defines a polling-based integration flow for periodic message generation. @@ -45,9 +44,9 @@ open class PollerConfig { * @return An IntegrationFlow that periodically generates and logs timestamps, then discards them. */ @Bean - open fun pollerFlow() = + fun pollerFlow() = integrationFlow( - MessageProcessorMessageSource { System.currentTimeMillis() }, + { System.currentTimeMillis() }, { poller { it.fixedDelay(20000).maxMessagesPerPoll(2) } }) { log(LoggingHandler.Level.INFO, "org.springframework.integration.samples.helloworld") channel(NullChannel()) diff --git a/basic/helloworld-kotlin/src/main/resources/log4j2.xml b/basic/helloworld-kotlin/src/main/resources/log4j2.xml index 9cb8aea38..7545f857d 100644 --- a/basic/helloworld-kotlin/src/main/resources/log4j2.xml +++ b/basic/helloworld-kotlin/src/main/resources/log4j2.xml @@ -6,8 +6,9 @@ - - + + + diff --git a/build.gradle b/build.gradle index e8160d3fe..481838f24 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ buildscript { classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" classpath 'org.gretty:gretty:4.1.10' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21' + classpath 'org.jetbrains.kotlin:kotlin-allopen:2.2.21' } } @@ -178,6 +179,56 @@ subprojects { subproject -> } } + if (subproject.plugins.hasPlugin('org.jetbrains.kotlin.jvm')) { + def kotlinVersion = '2.2.21' + def plugins = asNode().build?.find()?.plugins?.find() + if (!plugins) { + plugins = asNode().appendNode('build').with { + appendNode('sourceDirectory', '${project.basedir}/src/main/kotlin') + appendNode('testSourceDirectory', '${project.basedir}/src/test/kotlin') + appendNode('plugins') + } + } + + plugins.appendNode('plugin') + .with { + appendNode('groupId', 'org.jetbrains.kotlin') + appendNode('artifactId', 'kotlin-maven-plugin') + appendNode('version', kotlinVersion) + appendNode('configuration').with { + appendNode('args').with { + appendNode('arg', '-Xjsr305=strict') + } + appendNode('compilerPlugins').with { + appendNode('plugin', 'spring') + } + } + appendNode('executions').with { + appendNode('execution').with { + appendNode('id', 'compile') + appendNode('phase', 'compile') + appendNode('goals').with { + appendNode('goal', 'compile') + } + } + appendNode('execution').with { + appendNode('id', 'test-compile') + appendNode('phase', 'test-compile') + appendNode('goals').with { + appendNode('goal', 'test-compile') + } + } + } + appendNode('dependencies').with { + appendNode('dependency').with { + appendNode('groupId', 'org.jetbrains.kotlin') + appendNode('artifactId', 'kotlin-maven-allopen') + appendNode('version', kotlinVersion) + } + } + } + } + def pomDeps = asNode().dependencies.find() if (!pomDeps) { pomDeps = asNode().appendNode('dependencies') @@ -620,7 +671,8 @@ project('helloworld-groovy') { api "org.springframework.integration:spring-integration-groovy" testImplementation "org.apache.logging.log4j:log4j-core-test:$log4jVersion" testImplementation "org.awaitility:awaitility:$awaitilityVersion" - testImplementation("org.assertj:assertj-core:$assertjVersion") } + testImplementation("org.assertj:assertj-core:$assertjVersion") + } tasks.register ('runHelloWorldApp', JavaExec) { mainClass = 'org.springframework.integration.samples.helloworld.HelloWorldApp' @@ -637,7 +689,8 @@ project('helloworld-groovy') { project('helloworld-kotlin') { description = 'Hello World Sample for Kotlin Developers' - apply plugin: 'org.jetbrains.kotlin.jvm' + apply plugin: 'kotlin' + apply plugin: 'kotlin-spring' dependencies { api "org.apache.logging.log4j:log4j-core:$log4jVersion" @@ -646,7 +699,8 @@ project('helloworld-kotlin') { api "org.jetbrains.kotlin:kotlin-reflect" testImplementation "org.apache.logging.log4j:log4j-core-test:$log4jVersion" testImplementation "org.awaitility:awaitility:$awaitilityVersion" - testImplementation("org.assertj:assertj-core:$assertjVersion") } + testImplementation("org.assertj:assertj-core:$assertjVersion") + } tasks.register ('runHelloWorldApp', JavaExec) { mainClass = 'org.springframework.integration.samples.helloworld.HelloWorldApp' From 65c928f9103fcc5fee3139f34f746c6b90f44444 Mon Sep 17 00:00:00 2001 From: Glenn Renfro Date: Fri, 21 Nov 2025 14:42:05 -0500 Subject: [PATCH 4/6] Use global variable for kotlinVersion * Remove un used assertJ in groovy demo * Replace `org.jetbrains.kotlin.jvm` with `kotlin` when determining if kotlin plugin should be rended in pom.xml --- build.gradle | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 481838f24..5520997f1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,7 @@ buildscript { + ext { + kotlinVersion = '2.2.21' + } repositories { mavenCentral() gradlePluginPortal() @@ -9,8 +12,8 @@ buildscript { classpath 'io.spring.gradle:dependency-management-plugin:1.1.7' classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" classpath 'org.gretty:gretty:4.1.10' - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21' - classpath 'org.jetbrains.kotlin:kotlin-allopen:2.2.21' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlinVersion" } } @@ -20,6 +23,7 @@ apply plugin: 'base' apply plugin: 'idea' ext { + kotlinVersion = '2.2.21' linkHomepage = 'https://projects.spring.io/spring-integration' linkCi = 'https://build.spring.io/browse/INTSAMPLES' linkIssue = 'https://github.com/spring-projects/spring-integration-samples/issues' @@ -179,8 +183,7 @@ subprojects { subproject -> } } - if (subproject.plugins.hasPlugin('org.jetbrains.kotlin.jvm')) { - def kotlinVersion = '2.2.21' + if (subproject.plugins.hasPlugin('kotlin')) { def plugins = asNode().build?.find()?.plugins?.find() if (!plugins) { plugins = asNode().appendNode('build').with { @@ -194,7 +197,7 @@ subprojects { subproject -> .with { appendNode('groupId', 'org.jetbrains.kotlin') appendNode('artifactId', 'kotlin-maven-plugin') - appendNode('version', kotlinVersion) + appendNode('version', property('kotlinVersion')) appendNode('configuration').with { appendNode('args').with { appendNode('arg', '-Xjsr305=strict') @@ -223,7 +226,7 @@ subprojects { subproject -> appendNode('dependency').with { appendNode('groupId', 'org.jetbrains.kotlin') appendNode('artifactId', 'kotlin-maven-allopen') - appendNode('version', kotlinVersion) + appendNode('version', property('kotlinVersion')) } } } @@ -671,7 +674,6 @@ project('helloworld-groovy') { api "org.springframework.integration:spring-integration-groovy" testImplementation "org.apache.logging.log4j:log4j-core-test:$log4jVersion" testImplementation "org.awaitility:awaitility:$awaitilityVersion" - testImplementation("org.assertj:assertj-core:$assertjVersion") } tasks.register ('runHelloWorldApp', JavaExec) { From f002e8e3ccc4b3980732b18d7a56debb6955d6ed Mon Sep 17 00:00:00 2001 From: Glenn Renfro Date: Fri, 21 Nov 2025 15:54:51 -0500 Subject: [PATCH 5/6] Remove extraneoius kafkaVersion from ext --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5520997f1..16b23a149 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,6 @@ apply plugin: 'base' apply plugin: 'idea' ext { - kotlinVersion = '2.2.21' linkHomepage = 'https://projects.spring.io/spring-integration' linkCi = 'https://build.spring.io/browse/INTSAMPLES' linkIssue = 'https://github.com/spring-projects/spring-integration-samples/issues' From 4f97ca7648ef271c74ee0c23290ba733e5c1ccb5 Mon Sep 17 00:00:00 2001 From: Glenn Renfro Date: Fri, 21 Nov 2025 16:27:49 -0500 Subject: [PATCH 6/6] Add DirtiesContext to PollerConfigTests --- .../integration/samples/helloworld/PollerConfigTests.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/PollerConfigTests.kt b/basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/PollerConfigTests.kt index ae4ce636c..8b0dfe3de 100644 --- a/basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/PollerConfigTests.kt +++ b/basic/helloworld-kotlin/src/test/kotlin/org/springframework/integration/samples/helloworld/PollerConfigTests.kt @@ -26,6 +26,7 @@ import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.integration.dsl.IntegrationFlow +import org.springframework.test.annotation.DirtiesContext import org.springframework.test.context.junit.jupiter.SpringJUnitConfig import java.time.Duration @@ -37,6 +38,7 @@ import java.time.Duration * @author Glenn Renfro */ @SpringJUnitConfig(PollerConfig::class) +@DirtiesContext class PollerConfigTests { @Autowired