Skip to content

Commit 9302cb2

Browse files
konrad-kaminskisdeleuze
authored andcommitted
Support Kotlin suspending functions in MethodParameter
Before this commit, the return type for Kotlin suspending functions (as returned by MethodParameter#getParameterType and MethodParameter#getGenericReturnType methods) was incorrect. This change leverages Kotlin reflection instead of Java one to return the correct type. Closes gh-21058
1 parent 5938742 commit 9302cb2

File tree

3 files changed

+168
-4
lines changed

3 files changed

+168
-4
lines changed

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

+33-3
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,9 @@ public Class<?> getParameterType() {
402402
if (paramType == null) {
403403
if (this.parameterIndex < 0) {
404404
Method method = getMethod();
405-
paramType = (method != null ? method.getReturnType() : void.class);
405+
paramType = (method != null ?
406+
(KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(getContainingClass()) ?
407+
KotlinDelegate.getReturnType(method) : method.getReturnType()) : void.class);
406408
}
407409
else {
408410
paramType = this.executable.getParameterTypes()[this.parameterIndex];
@@ -422,7 +424,9 @@ public Type getGenericParameterType() {
422424
if (paramType == null) {
423425
if (this.parameterIndex < 0) {
424426
Method method = getMethod();
425-
paramType = (method != null ? method.getGenericReturnType() : void.class);
427+
paramType = (method != null ?
428+
(KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(getContainingClass()) ?
429+
KotlinDelegate.getGenericReturnType(method) : method.getGenericReturnType()) : void.class);
426430
}
427431
else {
428432
Type[] genericParameterTypes = this.executable.getGenericParameterTypes();
@@ -799,6 +803,32 @@ else if (ctor != null) {
799803
}
800804
return false;
801805
}
802-
}
803806

807+
/**
808+
* Return the generic return type of the method, with support of suspending
809+
* functions via Kotlin reflection.
810+
*/
811+
static private Type getGenericReturnType(Method method) {
812+
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
813+
if (function != null && function.isSuspend()) {
814+
return ReflectJvmMapping.getJavaType(function.getReturnType());
815+
}
816+
return method.getGenericReturnType();
817+
}
818+
819+
/**
820+
* Return the return type of the method, with support of suspending
821+
* functions via Kotlin reflection.
822+
*/
823+
static private Class<?> getReturnType(Method method) {
824+
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
825+
if (function != null && function.isSuspend()) {
826+
Type paramType = ReflectJvmMapping.getJavaType(function.getReturnType());
827+
Class<?> paramClass = ResolvableType.forType(paramType).resolve();
828+
Assert.notNull(paramClass, "Type " + paramType + "can't be resolved to a class");
829+
return paramClass;
830+
}
831+
return method.getReturnType();
832+
}
833+
}
804834
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2002-2019 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 [GenericTypeResolver].
26+
*
27+
* @author Konrad Kaminski
28+
* @author Sebastien Deleuze
29+
*/
30+
class KotlinGenericTypeResolverTests {
31+
32+
@Test
33+
fun methodReturnTypes() {
34+
assertEquals(Integer::class.java, resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "integer")!!,
35+
MyInterfaceType::class.java))
36+
assertEquals(String::class.java, resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "string")!!,
37+
MyInterfaceType::class.java))
38+
assertEquals(null, resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "raw")!!,
39+
MyInterfaceType::class.java))
40+
assertEquals(null, resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "object")!!,
41+
MyInterfaceType::class.java))
42+
}
43+
44+
private fun findMethod(clazz: Class<*>, name: String): Method? =
45+
clazz.methods.firstOrNull { it.name == name }
46+
47+
open class MyTypeWithMethods<T> {
48+
suspend fun integer(): MyInterfaceType<Int>? = null
49+
50+
suspend fun string(): MySimpleInterfaceType? = null
51+
52+
suspend fun `object`(): Any? = null
53+
54+
suspend fun raw(): MyInterfaceType<*>? = null
55+
}
56+
57+
interface MyInterfaceType<T>
58+
59+
interface MySimpleInterfaceType: MyInterfaceType<String>
60+
61+
open class MySimpleTypeWithMethods: MyTypeWithMethods<Int>()
62+
}

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

+73-1
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,22 @@
1515
*/
1616
package org.springframework.core
1717

18+
import org.junit.Assert.assertEquals
19+
import org.junit.Assert.assertFalse
20+
import org.junit.Assert.assertTrue
1821
import org.junit.Test
19-
import org.junit.Assert.*
2022
import java.lang.reflect.Method
23+
import java.lang.reflect.TypeVariable
24+
import kotlin.reflect.full.declaredFunctions
25+
import kotlin.reflect.jvm.javaMethod
2126

2227
/**
2328
* Tests for Kotlin support in [MethodParameter].
2429
*
2530
* @author Raman Gupta
2631
* @author Sebastien Deleuze
2732
* @author Juergen Hoeller
33+
* @author Konrad Kaminski
2834
*/
2935
class KotlinMethodParameterTests {
3036

@@ -67,6 +73,43 @@ class KotlinMethodParameterTests {
6773
assertTrue(MethodParameter(regularClassConstructor, 1).isOptional)
6874
}
6975

76+
@Test
77+
fun `Suspending function return type`() {
78+
assertEquals(Number::class.java, returnParameterType("suspendFun"))
79+
assertEquals(Number::class.java, returnGenericParameterType("suspendFun"))
80+
81+
assertEquals(Producer::class.java, returnParameterType("suspendFun2"))
82+
assertEquals("org.springframework.core.Producer<? extends java.lang.Number>", returnGenericParameterTypeName("suspendFun2"))
83+
84+
assertEquals(Wrapper::class.java, returnParameterType("suspendFun3"))
85+
assertEquals("org.springframework.core.Wrapper<java.lang.Number>", returnGenericParameterTypeName("suspendFun3"))
86+
87+
assertEquals(Consumer::class.java, returnParameterType("suspendFun4"))
88+
assertEquals("org.springframework.core.Consumer<? super java.lang.Number>", returnGenericParameterTypeName("suspendFun4"))
89+
90+
assertEquals(Producer::class.java, returnParameterType("suspendFun5"))
91+
assertTrue(returnGenericParameterType("suspendFun5") is TypeVariable<*>)
92+
assertEquals("org.springframework.core.Producer<? extends java.lang.Number>", returnGenericParameterTypeBoundName("suspendFun5"))
93+
94+
assertEquals(Wrapper::class.java, returnParameterType("suspendFun6"))
95+
assertTrue(returnGenericParameterType("suspendFun6") is TypeVariable<*>)
96+
assertEquals("org.springframework.core.Wrapper<java.lang.Number>", returnGenericParameterTypeBoundName("suspendFun6"))
97+
98+
assertEquals(Consumer::class.java, returnParameterType("suspendFun7"))
99+
assertTrue(returnGenericParameterType("suspendFun7") is TypeVariable<*>)
100+
assertEquals("org.springframework.core.Consumer<? super java.lang.Number>", returnGenericParameterTypeBoundName("suspendFun7"))
101+
102+
assertEquals(Object::class.java, returnParameterType("suspendFun8"))
103+
assertEquals(Object::class.java, returnGenericParameterType("suspendFun8"))
104+
}
105+
106+
private fun returnParameterType(funName: String) = returnMethodParameter(funName).parameterType
107+
private fun returnGenericParameterType(funName: String) = returnMethodParameter(funName).genericParameterType
108+
private fun returnGenericParameterTypeName(funName: String) = returnGenericParameterType(funName).typeName
109+
private fun returnGenericParameterTypeBoundName(funName: String) = (returnGenericParameterType(funName) as TypeVariable<*>).bounds[0].typeName
110+
111+
private fun returnMethodParameter(funName: String) =
112+
MethodParameter(this::class.declaredFunctions.first { it.name == funName }.javaMethod!!, -1)
70113

71114
@Suppress("unused_parameter")
72115
fun nullable(nullable: String?): Int? = 42
@@ -82,4 +125,33 @@ class KotlinMethodParameterTests {
82125
@Suppress("unused_parameter")
83126
class RegularClass(nonNullable: String, nullable: String?)
84127

128+
@Suppress("unused", "unused_parameter")
129+
suspend fun suspendFun(p1: String): Number = TODO()
130+
131+
@Suppress("unused", "unused_parameter")
132+
suspend fun suspendFun2(p1: String): Producer<Number> = TODO()
133+
134+
@Suppress("unused", "unused_parameter")
135+
suspend fun suspendFun3(p1: String): Wrapper<Number> = TODO()
136+
137+
@Suppress("unused", "unused_parameter")
138+
suspend fun suspendFun4(p1: String): Consumer<Number> = TODO()
139+
140+
@Suppress("unused", "unused_parameter")
141+
suspend fun <T: Producer<Number>> suspendFun5(p1: String): T = TODO()
142+
143+
@Suppress("unused", "unused_parameter")
144+
suspend fun <T: Wrapper<Number>> suspendFun6(p1: String): T = TODO()
145+
146+
@Suppress("unused", "unused_parameter")
147+
suspend fun <T: Consumer<Number>> suspendFun7(p1: String): T = TODO()
148+
149+
@Suppress("unused", "unused_parameter")
150+
suspend fun suspendFun8(p1: String): Any? = TODO()
85151
}
152+
153+
interface Producer<out T>
154+
155+
interface Wrapper<T>
156+
157+
interface Consumer<in T>

0 commit comments

Comments
 (0)