Skip to content

Commit aace662

Browse files
AdrienOleg Z
authored andcommitted
This commit introduces a comprehensive suite of tests to cover all possible declarations of Kotlin functions, consumers, and suppliers within the spring-cloud-function-kotlin module.
Test: Add arity tests for Kotlin functions, consumers, and suppliers The primary goal of these tests is to explore the various declaration combinations and identify potential areas for improvement and enhanced support in the framework. Signed-off-by: Adrien Poupard <[email protected]> Feat: Enhance Kotlin arity handling in function context This commit refactors the `spring-cloud-function-context` to comprehensively handle all combinations of possible declarations for Kotlin functions, consumers, and suppliers. This improves the framework's ability to correctly interpret and manage different Kotlin functional styles. Signed-off-by: Adrien Poupard <[email protected]> Test: Add unit tests for Kotlin arity and wrapper functionalities This commit adds new unit tests to verify the core logic for Kotlin arity handling and wrapper mechanisms. These tests cover various scenarios including catalogue registration and native type casting for consumers, functions, and suppliers. Signed-off-by: Adrien Poupard <[email protected]> Docs: Update programming model with enhanced Kotlin support details Signed-off-by: Adrien Poupard <[email protected]> Migrate kotlin `TypeUtils` methods to java `KotlinUtils` for improved Kotlin type handling and consistency. Signed-off-by: Adrien Poupard <[email protected]> Refactor: Replace FunctionUtils with KotlinUtils for consumer and function validation Signed-off-by: Adrien Poupard <[email protected]> Resolves #1278
1 parent 9cd37ed commit aace662

File tree

60 files changed

+8155
-362
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+8155
-362
lines changed

docs/modules/ROOT/pages/spring-cloud-function/programming-model.adoc

Lines changed: 158 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -705,31 +705,177 @@ However, given that `org.springframework.cloud.function.json.JsonMapper` is also
705705
[[kotlin-lambda-support]]
706706
== Kotlin Lambda support
707707

708-
We also provide support for Kotlin lambdas (since v2.0).
709-
Consider the following:
708+
Spring Cloud Function provides first-class support for Kotlin, allowing developers to leverage idiomatic Kotlin features, including coroutines and Flow, alongside imperative and Reactor-based programming models.
710709

711-
[source, java]
710+
=== Defining Functions in Kotlin
711+
712+
You can define Suppliers, Functions, and Consumers in Kotlin and register them as Spring beans using several approaches:
713+
714+
* **Kotlin Lambdas:** Define functions directly as lambda expressions within `@Bean` definitions. This is concise for simple functions.
715+
[source, kotlin]
716+
----
717+
@Configuration
718+
class MyKotlinConfiguration {
719+
720+
@Bean
721+
fun kotlinSupplier(): () -> String = { "Hello from Kotlin Lambda" }
722+
723+
@Bean
724+
fun kotlinFunction(): (String) -> String = { it.uppercase() }
725+
726+
@Bean
727+
fun kotlinConsumer(): (String) -> Unit = { println("Consumed by Kotlin Lambda: $it") }
728+
}
729+
----
730+
731+
* **Kotlin Classes implementing Kotlin Functional Types:** Define a class that directly implements the desired Kotlin functional type (e.g., `(String) -> String`, `suspend () -> Flow<Int>`).
732+
[source, kotlin]
733+
----
734+
@Component
735+
class UppercaseFunction : (String) -> String {
736+
override fun invoke(p1: String): String = p1.uppercase()
737+
}
738+
739+
// Can also be registered via @Bean
740+
----
741+
742+
* **Kotlin Classes implementing `java.util.function` Interfaces:** Define a Kotlin class that implements the standard Java `Supplier`, `Function`, or `Consumer` interfaces.
743+
[source, kotlin]
744+
----
745+
@Component
746+
class ReverseFunction : Function<String, String> {
747+
override fun apply(t: String): String = t.reversed()
748+
}
749+
----
750+
751+
Regardless of the definition style, beans of these types are registered with the `FunctionCatalog`, benefiting from features like type conversion and composition.
752+
753+
=== Coroutine Support (`suspend` and `Flow`)
754+
755+
A key feature is the seamless integration with Kotlin Coroutines. You can use `suspend` functions and `kotlinx.coroutines.flow.Flow` directly in your function signatures. The framework automatically handles the coroutine context and reactive stream conversions.
756+
757+
* **`suspend` Functions:** Functions marked with `suspend` can perform non-blocking operations using coroutine delays or other suspending calls.
758+
[source, kotlin]
712759
----
713760
@Bean
714-
open fun kotlinSupplier(): () -> String {
715-
return { "Hello from Kotlin" }
761+
fun suspendingFunction(): suspend (String) -> Int = {
762+
delay(100) // Non-blocking delay
763+
it.length
716764
}
717765
718766
@Bean
719-
open fun kotlinFunction(): (String) -> String {
720-
return { it.toUpperCase() }
767+
fun suspendingSupplier(): suspend () -> String = {
768+
delay(50)
769+
"Data from suspend"
721770
}
722771
723772
@Bean
724-
open fun kotlinConsumer(): (String) -> Unit {
725-
return { println(it) }
773+
fun suspendingConsumer(): suspend (String) -> Unit = {
774+
delay(20)
775+
println("Suspend consumed: $it")
726776
}
777+
----
727778

779+
* **`Flow` Integration:** Kotlin `Flow` can be used for reactive stream processing, similar to Reactor's `Flux`.
780+
[source, kotlin]
728781
----
729-
The above represents Kotlin lambdas configured as Spring beans. The signature of each maps to a Java equivalent of `Supplier`, `Function` and `Consumer`, and thus supported/recognized signatures by the framework.
730-
While mechanics of Kotlin-to-Java mapping are outside of the scope of this documentation, it is important to understand that the same rules for signature transformation outlined in "Java 8 function support" section are applied here as well.
782+
@Bean
783+
fun flowFunction(): (Flow<String>) -> Flow<Int> = { flow ->
784+
flow.map { it.length } // Process the stream reactively
785+
}
786+
787+
@Bean
788+
fun flowSupplier(): () -> Flow<String> = {
789+
flow { // kotlinx.coroutines.flow.flow builder
790+
emit("a")
791+
delay(10)
792+
emit("b")
793+
}
794+
}
795+
796+
// Consumer example taking a Flow
797+
@Bean
798+
fun flowConsumer(): suspend (Flow<String>) -> Unit = { flow ->
799+
flow.collect { item -> // Collect must happen within a coroutine scope
800+
println("Flow consumed: $item")
801+
}
802+
}
803+
----
804+
805+
* **Combining `suspend` and `Flow`:** You can combine `suspend` and `Flow` for complex asynchronous and streaming logic.
806+
[source, kotlin]
807+
----
808+
@Bean
809+
fun suspendingFlowFunction(): suspend (Flow<String>) -> Flow<String> = { incoming ->
810+
flow {
811+
delay(50) // Initial suspend
812+
incoming.collect {
813+
emit(it.uppercase()) // Process and emit
814+
}
815+
}
816+
}
817+
818+
@Bean
819+
fun suspendingFlowSupplier(): suspend () -> Flow<Int> = {
820+
flow {
821+
repeat(3) {
822+
delay(100)
823+
emit(it)
824+
}
825+
}
826+
}
827+
----
828+
829+
=== Reactive Types (`Mono`/`Flux`)
830+
831+
Kotlin functions can seamlessly use Reactor's `Mono` and `Flux` types, just like Java functions.
832+
[source, kotlin]
833+
----
834+
@Bean
835+
fun reactorFunction(): (Flux<String>) -> Mono<Int> = { flux ->
836+
flux.map { it.length }.reduce(0) { acc, i -> acc + i }
837+
}
838+
839+
@Bean
840+
fun monoSupplier(): () -> Mono<String> = {
841+
Mono.just("Reactive Hello")
842+
}
843+
----
844+
845+
=== `Message<T>` Support
846+
847+
Kotlin functions can also operate directly on `org.springframework.messaging.Message<T>` to access headers, including combinations with `suspend` and `Flow`.
848+
[source, kotlin]
849+
----
850+
@Bean
851+
fun messageFunction(): (Message<String>) -> Message<Int> = { msg ->
852+
MessageBuilder.withPayload(msg.payload.length)
853+
.copyHeaders(msg.headers)
854+
.setHeader("processed", true)
855+
.build()
856+
}
857+
858+
@Bean
859+
fun suspendMessageFunction(): suspend (Message<String>) -> Message<String> = { msg ->
860+
delay(100)
861+
MessageBuilder.withPayload(msg.payload.reversed())
862+
.copyHeaders(msg.headers)
863+
.build()
864+
}
865+
866+
@Bean
867+
fun flowMessageFunction(): (Flow<Message<String>>) -> Flow<Message<Int>> = { flow ->
868+
flow.map { msg ->
869+
MessageBuilder.withPayload(msg.payload.hashCode())
870+
.copyHeaders(msg.headers)
871+
.build()
872+
}
873+
}
874+
----
875+
876+
=== Kotlin Sample Project
731877

732-
To enable Kotlin support all you need is to add Kotlin SDK libraries on the classpath which will trigger appropriate autoconfiguration and supporting classes.
878+
For a comprehensive set of runnable examples showcasing these Kotlin features, please refer to the `src/test/kotlin/org/springframework/cloud/function/kotlin/arity` directory within the Spring Cloud Function repository. These examples demonstrate a wide range of function signatures with different arities, including regular functions, suspend functions (coroutines), and various reactive types (Flow, Mono, Flux).
733879

734880
[[function-component-scan]]
735881
== Function Component Scan

spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionRegistration.java

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
import org.springframework.beans.factory.BeanNameAware;
3333
import org.springframework.cloud.function.context.catalog.FunctionTypeUtils;
34-
import org.springframework.cloud.function.context.config.KotlinLambdaToFunctionAutoConfiguration;
34+
import org.springframework.cloud.function.context.wrapper.KotlinFunctionWrapper;
3535
import org.springframework.core.KotlinDetector;
3636
import org.springframework.util.Assert;
3737
import org.springframework.util.CollectionUtils;
@@ -118,7 +118,7 @@ public FunctionRegistration<T> properties(Map<String, String> properties) {
118118

119119
public FunctionRegistration<T> type(Type type) {
120120
this.type = type;
121-
if (KotlinDetector.isKotlinPresent() && this.target instanceof KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper) {
121+
if (KotlinDetector.isKotlinPresent() && this.target instanceof KotlinFunctionWrapper) {
122122
return this;
123123
}
124124
Type discoveredFunctionType = type; //FunctionTypeUtils.discoverFunctionTypeFromClass(this.target.getClass());
@@ -174,15 +174,6 @@ public FunctionRegistration<T> names(String... names) {
174174
return this.names(Arrays.asList(names));
175175
}
176176

177-
/**
178-
* Transforms (wraps) function identified by the 'target' to its {@code Flux}
179-
* equivalent unless it already is. For example, {@code Function<String, String>}
180-
* becomes {@code Function<Flux<String>, Flux<String>>}
181-
* @param <S> the expected target type of the function (e.g., FluxFunction)
182-
* @return {@code FunctionRegistration} with the appropriately wrapped target.
183-
*
184-
*/
185-
186177
@Override
187178
public void setBeanName(String name) {
188179
if (CollectionUtils.isEmpty(this.names)) {

spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistry.java

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@
3636
import org.springframework.beans.BeansException;
3737
import org.springframework.beans.factory.BeanFactory;
3838
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
39+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
3940
import org.springframework.cloud.function.context.FunctionProperties;
4041
import org.springframework.cloud.function.context.FunctionRegistration;
4142
import org.springframework.cloud.function.context.FunctionRegistry;
4243
import org.springframework.cloud.function.context.config.FunctionContextUtils;
43-
import org.springframework.cloud.function.context.config.KotlinLambdaToFunctionAutoConfiguration;
44+
import org.springframework.cloud.function.context.config.KotlinLambdaToFunctionFactory;
4445
import org.springframework.cloud.function.context.config.RoutingFunction;
4546
import org.springframework.cloud.function.core.FunctionInvocationHelper;
4647
import org.springframework.cloud.function.json.JsonMapper;
@@ -120,7 +121,8 @@ public <T> T lookup(Class<?> type, String functionDefinition, String... expected
120121
functionDefinition = StringUtils.hasText(functionDefinition)
121122
? functionDefinition
122123
: this.applicationContext.getEnvironment().getProperty(FunctionProperties.FUNCTION_DEFINITION, "");
123-
if (!this.applicationContext.containsBean(functionDefinition) || !KotlinUtils.isKotlinType(this.applicationContext.getBean(functionDefinition))) {
124+
125+
if (!this.applicationContext.containsBean(functionDefinition) || !isKotlinType(functionDefinition)) {
124126
functionDefinition = this.normalizeFunctionDefinition(functionDefinition);
125127
}
126128
if (!isFunctionDefinitionEligible(functionDefinition)) {
@@ -160,12 +162,9 @@ public <T> T lookup(Class<?> type, String functionDefinition, String... expected
160162
else if (functionCandidate instanceof BiFunction || functionCandidate instanceof BiConsumer) {
161163
functionRegistration = this.registerMessagingBiFunction(functionCandidate, functionName);
162164
}
163-
else if (KotlinUtils.isKotlinType(functionCandidate)) {
164-
KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper wrapper =
165-
new KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper(functionCandidate);
166-
wrapper.setName(functionName);
167-
wrapper.setBeanFactory(this.applicationContext.getBeanFactory());
168-
functionRegistration = wrapper.getFunctionRegistration();
165+
else if (isKotlinType(functionName, functionCandidate)) {
166+
KotlinLambdaToFunctionFactory kotlinFactory = new KotlinLambdaToFunctionFactory(functionCandidate, this.applicationContext.getBeanFactory());
167+
functionRegistration = kotlinFactory.getFunctionRegistration(functionName);
169168
}
170169
else if (this.isFunctionPojo(functionCandidate, functionName)) {
171170
Method functionalMethod = FunctionTypeUtils.discoverFunctionalMethod(functionCandidate.getClass());
@@ -203,6 +202,17 @@ else if (this.isSpecialFunctionRegistration(functionNames, functionName)) {
203202
return (T) function;
204203
}
205204

205+
private boolean isKotlinType(String functionDefinition) {
206+
Object fonctionBean = this.applicationContext.getBean(functionDefinition);
207+
return isKotlinType(functionDefinition, fonctionBean);
208+
}
209+
210+
private boolean isKotlinType(String functionDefinition, Object fonctionBean) {
211+
ConfigurableListableBeanFactory beanFactory = this.applicationContext.getBeanFactory();
212+
Type functionType = FunctionContextUtils.findType(functionDefinition, beanFactory);
213+
return KotlinUtils.isKotlinType(fonctionBean, functionType);
214+
}
215+
206216
@SuppressWarnings({ "rawtypes", "unchecked" })
207217
private FunctionRegistration registerMessagingBiFunction(Object userFunction, String functionName) {
208218
Type biFunctionType = FunctionContextUtils.findType(this.applicationContext.getBeanFactory(), functionName);

spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtils.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import com.fasterxml.jackson.databind.JsonNode;
4949
import kotlin.jvm.functions.Function0;
5050
import kotlin.jvm.functions.Function1;
51+
import kotlin.jvm.functions.Function2;
5152
import org.apache.commons.logging.Log;
5253
import org.apache.commons.logging.LogFactory;
5354
import org.reactivestreams.Publisher;
@@ -234,6 +235,10 @@ else if (Function0.class.isAssignableFrom(functionalClass)) {
234235
ResolvableType kotlinType = ResolvableType.forClass(functionalClass).as(Function0.class);
235236
return GenericTypeResolver.resolveType(kotlinType.getType(), functionalClass);
236237
}
238+
else if (Function2.class.isAssignableFrom(functionalClass)) {
239+
ResolvableType kotlinType = ResolvableType.forClass(functionalClass).as(Function2.class);
240+
return GenericTypeResolver.resolveType(kotlinType.getType(), functionalClass);
241+
}
237242
}
238243
Type typeToReturn = null;
239244
if (Function.class.isAssignableFrom(functionalClass)) {

0 commit comments

Comments
 (0)