diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtils.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtils.java index 451d8b10c..3fc05f078 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtils.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtils.java @@ -68,6 +68,7 @@ import org.springframework.core.ResolvableType; import org.springframework.messaging.Message; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -166,10 +167,13 @@ public final class FunctionTypeUtils { * @return instance of {@link Class} as raw representation of the provided {@link Type} */ public static Class getRawType(Type type) { -// ((WildcardType) type).getUpperBounds(); -// Class cazz = ResolvableType.forType(type).getRawClass(); if (type instanceof WildcardType) { - return Object.class; + Type[] upperbounds = ((WildcardType) type).getUpperBounds(); + /* + * Kotlin may have something like this which is technically a whildcard yet it has upper/lower types. + * See GH-1260 + */ + return ObjectUtils.isEmpty(upperbounds) ? Object.class : getRawType(upperbounds[0]); } return ResolvableType.forType(type).getRawClass(); } @@ -392,7 +396,12 @@ public final class FunctionTypeUtils { resolvableInputType = resolvableFunctionType.as(Function.class); } else { - resolvableInputType = resolvableFunctionType.as(Consumer.class); + if (KotlinDetector.isKotlinPresent() && Function1.class.isAssignableFrom(getRawType(functionType))) { // Kotlin + return ResolvableType.forType(getImmediateGenericType(functionType, 1)).getType(); + } + else { + resolvableInputType = resolvableFunctionType.as(Consumer.class); + } } if (resolvableInputType.getType() instanceof ParameterizedType) { return resolvableInputType.getGeneric(0).getType(); @@ -477,7 +486,12 @@ public final class FunctionTypeUtils { resolvableOutputType = resolvableFunctionType.as(Function.class); } else { - resolvableOutputType = resolvableFunctionType.as(Supplier.class); + if (KotlinDetector.isKotlinPresent() && Function1.class.isAssignableFrom(getRawType(functionType))) { // Kotlin + return ResolvableType.forType(getImmediateGenericType(functionType, 1)).getType(); + } + else { + resolvableOutputType = resolvableFunctionType.as(Supplier.class); + } } Type outputType; @@ -629,6 +643,7 @@ public final class FunctionTypeUtils { Class candidateType = (Class) type; Assert.isTrue(Supplier.class.isAssignableFrom(candidateType) + || (KotlinDetector.isKotlinPresent() && (Function0.class.isAssignableFrom(candidateType) || Function1.class.isAssignableFrom(candidateType))) || Function.class.isAssignableFrom(candidateType) || Consumer.class.isAssignableFrom(candidateType) || FunctionRegistration.class.isAssignableFrom(candidateType) diff --git a/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinTypeDiscoveryTests.java b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinTypeDiscoveryTests.java new file mode 100644 index 000000000..769c0b221 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinTypeDiscoveryTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019-2025 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.cloud.function.kotlin; + + + +import java.lang.reflect.Type; + +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KotlinTypeDiscoveryTests { + + @Test + public void testOutputInputTypes() { + Type functionType = FunctionTypeUtils.discoverFunctionTypeFromClass(KotlinComponentMessageFunction.class); + Type outputType = FunctionTypeUtils.getOutputType(functionType); + assertThat(FunctionTypeUtils.isMessage(outputType)).isTrue(); + + Type inputType = FunctionTypeUtils.getInputType(functionType); + assertThat(FunctionTypeUtils.isMessage(inputType)).isTrue(); + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinComponentMessageFunction.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinComponentMessageFunction.kt new file mode 100644 index 000000000..7f04cc57c --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinComponentMessageFunction.kt @@ -0,0 +1,15 @@ +package org.springframework.cloud.function.kotlin + +import org.springframework.messaging.Message +import org.springframework.messaging.MessageHeaders +import org.springframework.messaging.support.MessageBuilder +import org.springframework.stereotype.Component + +import java.util.function.Function + +@Component +class KotlinComponentMessageFunction : (List>) -> List> { + override fun invoke(input: List>): List> { + return input + } +}