Skip to content

Commit 7617a01

Browse files
committed
Unwrap Kotlin inline value classes return values
The result returned by Kotlin reflective invocation of a function returning an inline value class is wrapped, which makes sense from Kotlin POV but from a JVM perspective the associated value and type should be unwrapped to be consistent with what would happen with a reflective invocation done by Java. This commit unwraps such result. Closes gh-33026
1 parent 82c5aa4 commit 7617a01

File tree

6 files changed

+67
-3
lines changed

6 files changed

+67
-3
lines changed

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

+20-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.reactivestreams.Publisher;
4545
import reactor.core.publisher.Flux;
4646
import reactor.core.publisher.Mono;
47+
import reactor.core.publisher.SynchronousSink;
4748

4849
import org.springframework.lang.Nullable;
4950
import org.springframework.util.Assert;
@@ -145,7 +146,7 @@ public static Publisher<?> invokeSuspendingFunction(
145146
}
146147
return KCallables.callSuspendBy(function, argMap, continuation);
147148
})
148-
.filter(result -> result != Unit.INSTANCE)
149+
.handle(CoroutinesUtils::handleResult)
149150
.onErrorMap(InvocationTargetException.class, InvocationTargetException::getTargetException);
150151

151152
KType returnType = function.getReturnType();
@@ -165,4 +166,22 @@ private static Flux<?> asFlux(Object flow) {
165166
return ReactorFlowKt.asFlux(((Flow<?>) flow));
166167
}
167168

169+
private static void handleResult(Object result, SynchronousSink<Object> sink) {
170+
if (result == Unit.INSTANCE) {
171+
sink.complete();
172+
}
173+
else if (KotlinDetector.isInlineClass(result.getClass())) {
174+
try {
175+
sink.next(result.getClass().getDeclaredMethod("unbox-impl").invoke(result));
176+
sink.complete();
177+
}
178+
catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
179+
sink.error(ex);
180+
}
181+
}
182+
else {
183+
sink.next(result);
184+
sink.complete();
185+
}
186+
}
168187
}

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

+14
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,15 @@ class CoroutinesUtilsTests {
199199
}
200200
}
201201

202+
@Test
203+
fun invokeSuspendingFunctionWithValueClassReturnValue() {
204+
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassReturnValue") }
205+
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, null) as Mono
206+
runBlocking {
207+
Assertions.assertThat(mono.awaitSingle()).isEqualTo("foo")
208+
}
209+
}
210+
202211
@Test
203212
fun invokeSuspendingFunctionWithValueClassWithInitParameter() {
204213
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassWithInit") }
@@ -310,6 +319,11 @@ class CoroutinesUtilsTests {
310319
return value.value
311320
}
312321

322+
suspend fun suspendingFunctionWithValueClassReturnValue(): ValueClass {
323+
delay(1)
324+
return ValueClass("foo")
325+
}
326+
313327
suspend fun suspendingFunctionWithValueClassWithInit(value: ValueClassWithInit): String {
314328
delay(1)
315329
return value.value

spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ private static class KotlinDelegate {
299299

300300
@Nullable
301301
@SuppressWarnings({"deprecation", "DataFlowIssue"})
302-
public static Object invokeFunction(Method method, Object target, Object[] args) throws InvocationTargetException, IllegalAccessException {
302+
public static Object invokeFunction(Method method, Object target, Object[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
303303
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
304304
// For property accessors
305305
if (function == null) {
@@ -332,6 +332,9 @@ public static Object invokeFunction(Method method, Object target, Object[] args)
332332
}
333333
}
334334
Object result = function.callBy(argMap);
335+
if (result != null && KotlinDetector.isInlineClass(result.getClass())) {
336+
return result.getClass().getDeclaredMethod("unbox-impl").invoke(result);
337+
}
335338
return (result == Unit.INSTANCE ? null : result);
336339
}
337340
}

spring-web/src/test/kotlin/org/springframework/web/method/support/InvocableHandlerMethodKotlinTests.kt

+12
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ class InvocableHandlerMethodKotlinTests {
104104
Assertions.assertThat(value).isEqualTo(1L)
105105
}
106106

107+
@Test
108+
fun valueClassReturnValue() {
109+
val value = getInvocable(ValueClassHandler::valueClassReturnValue.javaMethod!!).invokeForRequest(request, null)
110+
Assertions.assertThat(value).isEqualTo("foo")
111+
}
112+
107113
@Test
108114
fun valueClassDefaultValue() {
109115
composite.addResolver(StubArgumentResolver(Double::class.java))
@@ -200,6 +206,9 @@ class InvocableHandlerMethodKotlinTests {
200206

201207
private class ValueClassHandler {
202208

209+
fun valueClassReturnValue() =
210+
StringValueClass("foo")
211+
203212
fun longValueClass(limit: LongValueClass) =
204213
limit.value
205214

@@ -246,6 +255,9 @@ class InvocableHandlerMethodKotlinTests {
246255

247256
data class Animal(override val name: String) : Named
248257

258+
@JvmInline
259+
value class StringValueClass(val value: String)
260+
249261
@JvmInline
250262
value class LongValueClass(val value: Long)
251263

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ private static class KotlinDelegate {
325325
@Nullable
326326
@SuppressWarnings({"deprecation", "DataFlowIssue"})
327327
public static Object invokeFunction(Method method, Object target, Object[] args, boolean isSuspendingFunction,
328-
ServerWebExchange exchange) throws InvocationTargetException, IllegalAccessException {
328+
ServerWebExchange exchange) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
329329

330330
if (isSuspendingFunction) {
331331
Object coroutineContext = exchange.getAttribute(COROUTINE_CONTEXT_ATTRIBUTE);
@@ -369,6 +369,9 @@ public static Object invokeFunction(Method method, Object target, Object[] args,
369369
}
370370
}
371371
Object result = function.callBy(argMap);
372+
if (result != null && KotlinDetector.isInlineClass(result.getClass())) {
373+
return result.getClass().getDeclaredMethod("unbox-impl").invoke(result);
374+
}
372375
return (result == Unit.INSTANCE ? null : result);
373376
}
374377
}

spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt

+13
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,13 @@ class InvocableHandlerMethodKotlinTests {
205205
assertHandlerResultValue(result, "1")
206206
}
207207

208+
@Test
209+
fun valueClassReturnValue() {
210+
val method = ValueClassController::valueClassReturnValue.javaMethod!!
211+
val result = invoke(ValueClassController(), method,)
212+
assertHandlerResultValue(result, "foo")
213+
}
214+
208215
@Test
209216
fun valueClassWithDefaultValue() {
210217
this.resolvers.add(stubResolver(null, Double::class.java))
@@ -376,6 +383,9 @@ class InvocableHandlerMethodKotlinTests {
376383
fun valueClass(limit: LongValueClass) =
377384
"${limit.value}"
378385

386+
fun valueClassReturnValue() =
387+
StringValueClass("foo")
388+
379389
fun valueClassWithDefault(limit: DoubleValueClass = DoubleValueClass(3.1)) =
380390
"${limit.value}"
381391

@@ -420,6 +430,9 @@ class InvocableHandlerMethodKotlinTests {
420430

421431
data class Animal(override val name: String) : Named
422432

433+
@JvmInline
434+
value class StringValueClass(val value: String)
435+
423436
@JvmInline
424437
value class LongValueClass(val value: Long)
425438

0 commit comments

Comments
 (0)