Skip to content

Commit 7fc81ca

Browse files
committed
GH-1260 Additional fix to Kotlin type resolution for generics
This is specifically relevant to the way Kotlin represents types. For example List<Message> resolves to List <? extends Message> which becomes the WildCard unlike in Java Resolves #1260
1 parent 5c31eac commit 7fc81ca

File tree

3 files changed

+75
-5
lines changed

3 files changed

+75
-5
lines changed

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import org.springframework.messaging.Message;
6969
import org.springframework.util.Assert;
7070
import org.springframework.util.CollectionUtils;
71+
import org.springframework.util.ObjectUtils;
7172
import org.springframework.util.ReflectionUtils;
7273
import org.springframework.util.StringUtils;
7374

@@ -166,10 +167,13 @@ public static Type getGenericType(Type type) {
166167
* @return instance of {@link Class} as raw representation of the provided {@link Type}
167168
*/
168169
public static Class<?> getRawType(Type type) {
169-
// ((WildcardType) type).getUpperBounds();
170-
// Class cazz = ResolvableType.forType(type).getRawClass();
171170
if (type instanceof WildcardType) {
172-
return Object.class;
171+
Type[] upperbounds = ((WildcardType) type).getUpperBounds();
172+
/*
173+
* Kotlin may have something like this <? extends Message> which is technically a whildcard yet it has upper/lower types.
174+
* See GH-1260
175+
*/
176+
return ObjectUtils.isEmpty(upperbounds) ? Object.class : getRawType(upperbounds[0]);
173177
}
174178
return ResolvableType.forType(type).getRawClass();
175179
}
@@ -392,7 +396,12 @@ public static Type getInputType(Type functionType) {
392396
resolvableInputType = resolvableFunctionType.as(Function.class);
393397
}
394398
else {
395-
resolvableInputType = resolvableFunctionType.as(Consumer.class);
399+
if (KotlinDetector.isKotlinPresent() && Function1.class.isAssignableFrom(getRawType(functionType))) { // Kotlin
400+
return ResolvableType.forType(getImmediateGenericType(functionType, 1)).getType();
401+
}
402+
else {
403+
resolvableInputType = resolvableFunctionType.as(Consumer.class);
404+
}
396405
}
397406
if (resolvableInputType.getType() instanceof ParameterizedType) {
398407
return resolvableInputType.getGeneric(0).getType();
@@ -477,7 +486,12 @@ public static Type getOutputType(Type functionType) {
477486
resolvableOutputType = resolvableFunctionType.as(Function.class);
478487
}
479488
else {
480-
resolvableOutputType = resolvableFunctionType.as(Supplier.class);
489+
if (KotlinDetector.isKotlinPresent() && Function1.class.isAssignableFrom(getRawType(functionType))) { // Kotlin
490+
return ResolvableType.forType(getImmediateGenericType(functionType, 1)).getType();
491+
}
492+
else {
493+
resolvableOutputType = resolvableFunctionType.as(Supplier.class);
494+
}
481495
}
482496

483497
Type outputType;
@@ -629,6 +643,7 @@ private static void assertSupportedTypes(Type type) {
629643
Class<?> candidateType = (Class<?>) type;
630644

631645
Assert.isTrue(Supplier.class.isAssignableFrom(candidateType)
646+
|| (KotlinDetector.isKotlinPresent() && (Function0.class.isAssignableFrom(candidateType) || Function1.class.isAssignableFrom(candidateType)))
632647
|| Function.class.isAssignableFrom(candidateType)
633648
|| Consumer.class.isAssignableFrom(candidateType)
634649
|| FunctionRegistration.class.isAssignableFrom(candidateType)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2019-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.function.kotlin;
18+
19+
20+
21+
import java.lang.reflect.Type;
22+
23+
import org.junit.jupiter.api.Test;
24+
25+
import org.springframework.cloud.function.context.catalog.FunctionTypeUtils;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
public class KotlinTypeDiscoveryTests {
30+
31+
@Test
32+
public void testOutputInputTypes() {
33+
Type functionType = FunctionTypeUtils.discoverFunctionTypeFromClass(KotlinComponentMessageFunction.class);
34+
Type outputType = FunctionTypeUtils.getOutputType(functionType);
35+
assertThat(FunctionTypeUtils.isMessage(outputType)).isTrue();
36+
37+
Type inputType = FunctionTypeUtils.getInputType(functionType);
38+
assertThat(FunctionTypeUtils.isMessage(inputType)).isTrue();
39+
}
40+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.springframework.cloud.function.kotlin
2+
3+
import org.springframework.messaging.Message
4+
import org.springframework.messaging.MessageHeaders
5+
import org.springframework.messaging.support.MessageBuilder
6+
import org.springframework.stereotype.Component
7+
8+
import java.util.function.Function
9+
10+
@Component
11+
class KotlinComponentMessageFunction : (List<Message<Char>>) -> List<Message<Char>> {
12+
override fun invoke(input: List<Message<Char>>): List<Message<Char>> {
13+
return input
14+
}
15+
}

0 commit comments

Comments
 (0)