Skip to content

Commit 821481a

Browse files
Return correct return type for Kotlin suspending functions in MethodParameter.
Return type for Kotlin suspending functions (as returned by MethodParameter#getParameterType and MethodParameter#getgenericReturnType methods) is incorrect. The true return type is the generic parameter of the last parameter of the method. This change modifies the behaviour of the aforementioned methods so that they work correctly for all cases. Issue: SPR-16515
1 parent 265960f commit 821481a

File tree

3 files changed

+184
-7
lines changed

3 files changed

+184
-7
lines changed

spring-core/src/main/java/org/springframework/core/MethodParameter.java

+49-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.lang.reflect.Parameter;
2626
import java.lang.reflect.ParameterizedType;
2727
import java.lang.reflect.Type;
28+
import java.lang.reflect.WildcardType;
2829
import java.util.HashMap;
2930
import java.util.List;
3031
import java.util.Map;
@@ -396,7 +397,9 @@ public Class<?> getParameterType() {
396397
if (paramType == null) {
397398
if (this.parameterIndex < 0) {
398399
Method method = getMethod();
399-
paramType = (method != null ? method.getReturnType() : void.class);
400+
paramType = (method != null ?
401+
(KotlinDetector.isKotlinType(getContainingClass()) ?
402+
KotlinDelegate.getReturnType(method) : method.getReturnType()) : void.class);
400403
}
401404
else {
402405
paramType = this.executable.getParameterTypes()[this.parameterIndex];
@@ -416,7 +419,9 @@ public Type getGenericParameterType() {
416419
if (paramType == null) {
417420
if (this.parameterIndex < 0) {
418421
Method method = getMethod();
419-
paramType = (method != null ? method.getGenericReturnType() : void.class);
422+
paramType = (method != null ?
423+
(KotlinDetector.isKotlinType(getContainingClass()) ?
424+
KotlinDelegate.getGenericReturnType(method) : method.getGenericReturnType()) : void.class);
420425
}
421426
else {
422427
Type[] genericParameterTypes = this.executable.getGenericParameterTypes();
@@ -777,6 +782,47 @@ else if (ctor != null) {
777782
}
778783
return false;
779784
}
780-
}
781785

786+
/**
787+
* Returns a return type of a method using Kotlin Reflection API.
788+
* Introduced to support suspending functions.
789+
*/
790+
static private Class<?> getReturnType(Method method) {
791+
if (isSuspend(method)) {
792+
final Class<?> returnType = getSuspendReturnType(method).resolve();
793+
Assert.notNull(returnType, "returnType cannot be null");
794+
return returnType;
795+
} else {
796+
return method.getReturnType();
797+
}
798+
}
799+
800+
/**
801+
* Returns a generic return type of a method using Kotlin Reflection API.
802+
* Introduced to support suspending functions.
803+
*/
804+
static private Type getGenericReturnType(Method method) {
805+
if (isSuspend(method)) {
806+
return getSuspendReturnType(method).getType();
807+
} else {
808+
return method.getGenericReturnType();
809+
}
810+
}
811+
812+
static private boolean isSuspend(Method method) {
813+
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
814+
return (function != null && function.isSuspend());
815+
}
816+
817+
static private ResolvableType getSuspendReturnType(Method method) {
818+
ResolvableType cgeneric = ResolvableType.forMethodParameter(method, method.getParameterCount() - 1)
819+
.getGeneric(0);
820+
821+
if (cgeneric.getType() instanceof WildcardType) {
822+
return ResolvableType.forType(((WildcardType) cgeneric.getType()).getLowerBounds()[0]);
823+
} else {
824+
return cgeneric;
825+
}
826+
}
827+
}
782828
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2002-2018 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+
* http://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.core
18+
19+
import org.junit.Assert.assertEquals
20+
import org.junit.Test
21+
import org.springframework.core.GenericTypeResolver.resolveReturnTypeArgument
22+
import java.lang.reflect.Method
23+
24+
/**
25+
* Tests for Kotlin support in [MethodParameter.getGenericParameterType].
26+
*
27+
* @author Konrad Kaminski
28+
*/
29+
30+
class KotlinGenericTypeResolverTests {
31+
32+
@Test
33+
fun methodReturnTypes() {
34+
assertEquals(Integer::class.java,
35+
resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "integer")!!, MyInterfaceType::class.java))
36+
assertEquals(String::class.java,
37+
resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "string")!!, MyInterfaceType::class.java))
38+
assertEquals(null, resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "raw")!!, MyInterfaceType::class.java))
39+
assertEquals(null,
40+
resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "object")!!, MyInterfaceType::class.java))
41+
}
42+
43+
private fun findMethod(clazz: Class<*>, name: String): Method? =
44+
clazz.methods.firstOrNull { it.name == name }
45+
46+
open class MyTypeWithMethods<T> {
47+
suspend fun integer(): MyInterfaceType<Int>? = null
48+
49+
suspend fun string(): MySimpleInterfaceType? = null
50+
51+
suspend fun `object`(): Any? = null
52+
53+
suspend fun raw(): MyInterfaceType<*>? = null
54+
}
55+
56+
interface MyInterfaceType<T>
57+
58+
interface MySimpleInterfaceType: MyInterfaceType<String>
59+
60+
open class MySimpleTypeWithMethods: MyTypeWithMethods<Int>()
61+
}

spring-core/src/test/kotlin/org/springframework/core/KotlinMethodParameterTests.kt

+74-4
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,23 @@
1616

1717
package org.springframework.core
1818

19-
import java.lang.reflect.Method
20-
19+
import org.junit.Assert.assertEquals
20+
import org.junit.Assert.assertFalse
21+
import org.junit.Assert.assertTrue
2122
import org.junit.Before
2223
import org.junit.Test
23-
24-
import org.junit.Assert.*
24+
import java.lang.reflect.Method
25+
import java.lang.reflect.TypeVariable
26+
import kotlin.reflect.full.declaredFunctions
27+
import kotlin.reflect.jvm.javaMethod
2528

2629
/**
2730
* Tests for Kotlin support in [MethodParameter].
2831
*
2932
* @author Raman Gupta
3033
* @author Sebastien Deleuze
3134
* @author Juergen Hoeller
35+
* @author Konrad Kaminski
3236
*/
3337
class KotlinMethodParameterTests {
3438

@@ -57,11 +61,77 @@ class KotlinMethodParameterTests {
5761
assertFalse(MethodParameter(nonNullableMethod, -1).isOptional())
5862
}
5963

64+
@Test
65+
fun `Suspending function return type`() {
66+
assertEquals(Number::class.java, returnParameterType("suspendFun"))
67+
assertEquals(Number::class.java, returnGenericParameterType("suspendFun"))
68+
69+
assertEquals(Producer::class.java, returnParameterType("suspendFun2"))
70+
assertEquals("org.springframework.core.Producer<? extends java.lang.Number>", returnGenericParameterTypeName("suspendFun2"))
71+
72+
assertEquals(Wrapper::class.java, returnParameterType("suspendFun3"))
73+
assertEquals("org.springframework.core.Wrapper<java.lang.Number>", returnGenericParameterTypeName("suspendFun3"))
74+
75+
assertEquals(Consumer::class.java, returnParameterType("suspendFun4"))
76+
assertEquals("org.springframework.core.Consumer<? super java.lang.Number>", returnGenericParameterTypeName("suspendFun4"))
77+
78+
assertEquals(Producer::class.java, returnParameterType("suspendFun5"))
79+
assertTrue(returnGenericParameterType("suspendFun5") is TypeVariable<*>)
80+
assertEquals("org.springframework.core.Producer<? extends java.lang.Number>", returnGenericParameterTypeBoundName("suspendFun5"))
81+
82+
assertEquals(Wrapper::class.java, returnParameterType("suspendFun6"))
83+
assertTrue(returnGenericParameterType("suspendFun6") is TypeVariable<*>)
84+
assertEquals("org.springframework.core.Wrapper<java.lang.Number>", returnGenericParameterTypeBoundName("suspendFun6"))
85+
86+
assertEquals(Consumer::class.java, returnParameterType("suspendFun7"))
87+
assertTrue(returnGenericParameterType("suspendFun7") is TypeVariable<*>)
88+
assertEquals("org.springframework.core.Consumer<? super java.lang.Number>", returnGenericParameterTypeBoundName("suspendFun7"))
89+
90+
assertEquals(Object::class.java, returnParameterType("suspendFun8"))
91+
assertEquals(Object::class.java, returnGenericParameterType("suspendFun8"))
92+
}
93+
94+
private fun returnParameterType(funName: String) = returnMethodParameter(funName).parameterType
95+
private fun returnGenericParameterType(funName: String) = returnMethodParameter(funName).genericParameterType
96+
private fun returnGenericParameterTypeName(funName: String) = returnGenericParameterType(funName).typeName
97+
private fun returnGenericParameterTypeBoundName(funName: String) = (returnGenericParameterType(funName) as TypeVariable<*>).bounds[0].typeName
98+
99+
private fun returnMethodParameter(funName: String) =
100+
MethodParameter(this::class.declaredFunctions.first { it.name == funName }.javaMethod!!, -1)
60101

61102
@Suppress("unused", "unused_parameter")
62103
fun nullable(p1: String?): Int? = 42
63104

64105
@Suppress("unused", "unused_parameter")
65106
fun nonNullable(p1: String): Int = 42
66107

108+
@Suppress("unused", "unused_parameter")
109+
suspend fun suspendFun(p1: String): Number = TODO()
110+
111+
@Suppress("unused", "unused_parameter")
112+
suspend fun suspendFun2(p1: String): Producer<Number> = TODO()
113+
114+
@Suppress("unused", "unused_parameter")
115+
suspend fun suspendFun3(p1: String): Wrapper<Number> = TODO()
116+
117+
@Suppress("unused", "unused_parameter")
118+
suspend fun suspendFun4(p1: String): Consumer<Number> = TODO()
119+
120+
@Suppress("unused", "unused_parameter")
121+
suspend fun <T: Producer<Number>> suspendFun5(p1: String): T = TODO()
122+
123+
@Suppress("unused", "unused_parameter")
124+
suspend fun <T: Wrapper<Number>> suspendFun6(p1: String): T = TODO()
125+
126+
@Suppress("unused", "unused_parameter")
127+
suspend fun <T: Consumer<Number>> suspendFun7(p1: String): T = TODO()
128+
129+
@Suppress("unused", "unused_parameter")
130+
suspend fun suspendFun8(p1: String): Any? = TODO()
67131
}
132+
133+
interface Producer<out T>
134+
135+
interface Wrapper<T>
136+
137+
interface Consumer<in T>

0 commit comments

Comments
 (0)