diff --git a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java index fb345f8f715..3a0f582f50f 100644 --- a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java +++ b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java @@ -44,6 +44,7 @@ import kotlinx.coroutines.reactor.ReactorFlowKt; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.SynchronousSink; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -145,7 +146,7 @@ public abstract class CoroutinesUtils { } return KCallables.callSuspendBy(function, argMap, continuation); }) - .filter(result -> result != Unit.INSTANCE) + .handle(CoroutinesUtils::handleResult) .onErrorMap(InvocationTargetException.class, InvocationTargetException::getTargetException); KType returnType = function.getReturnType(); @@ -165,4 +166,22 @@ public abstract class CoroutinesUtils { return ReactorFlowKt.asFlux(((Flow) flow)); } + private static void handleResult(Object result, SynchronousSink sink) { + if (result == Unit.INSTANCE) { + sink.complete(); + } + else if (KotlinDetector.isInlineClass(result.getClass())) { + try { + sink.next(result.getClass().getDeclaredMethod("unbox-impl").invoke(result)); + sink.complete(); + } + catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) { + sink.error(ex); + } + } + else { + sink.next(result); + sink.complete(); + } + } } diff --git a/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt b/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt index 2940f1d3ff7..143a2cad08c 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt @@ -199,6 +199,15 @@ class CoroutinesUtilsTests { } } + @Test + fun invokeSuspendingFunctionWithValueClassReturnValue() { + val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassReturnValue") } + val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, null) as Mono + runBlocking { + Assertions.assertThat(mono.awaitSingle()).isEqualTo("foo") + } + } + @Test fun invokeSuspendingFunctionWithValueClassWithInitParameter() { val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassWithInit") } @@ -310,6 +319,11 @@ class CoroutinesUtilsTests { return value.value } + suspend fun suspendingFunctionWithValueClassReturnValue(): ValueClass { + delay(1) + return ValueClass("foo") + } + suspend fun suspendingFunctionWithValueClassWithInit(value: ValueClassWithInit): String { delay(1) return value.value diff --git a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java index 416d8a39099..6c84737f706 100644 --- a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java @@ -299,7 +299,7 @@ public class InvocableHandlerMethod extends HandlerMethod { @Nullable @SuppressWarnings({"deprecation", "DataFlowIssue"}) - public static Object invokeFunction(Method method, Object target, Object[] args) throws InvocationTargetException, IllegalAccessException { + public static Object invokeFunction(Method method, Object target, Object[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { KFunction function = ReflectJvmMapping.getKotlinFunction(method); // For property accessors if (function == null) { @@ -332,6 +332,9 @@ public class InvocableHandlerMethod extends HandlerMethod { } } Object result = function.callBy(argMap); + if (result != null && KotlinDetector.isInlineClass(result.getClass())) { + return result.getClass().getDeclaredMethod("unbox-impl").invoke(result); + } return (result == Unit.INSTANCE ? null : result); } } diff --git a/spring-web/src/test/kotlin/org/springframework/web/method/support/InvocableHandlerMethodKotlinTests.kt b/spring-web/src/test/kotlin/org/springframework/web/method/support/InvocableHandlerMethodKotlinTests.kt index 99ad7c8e0ea..60e52f4b1b5 100644 --- a/spring-web/src/test/kotlin/org/springframework/web/method/support/InvocableHandlerMethodKotlinTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/web/method/support/InvocableHandlerMethodKotlinTests.kt @@ -104,6 +104,12 @@ class InvocableHandlerMethodKotlinTests { Assertions.assertThat(value).isEqualTo(1L) } + @Test + fun valueClassReturnValue() { + val value = getInvocable(ValueClassHandler::valueClassReturnValue.javaMethod!!).invokeForRequest(request, null) + Assertions.assertThat(value).isEqualTo("foo") + } + @Test fun valueClassDefaultValue() { composite.addResolver(StubArgumentResolver(Double::class.java)) @@ -200,6 +206,9 @@ class InvocableHandlerMethodKotlinTests { private class ValueClassHandler { + fun valueClassReturnValue() = + StringValueClass("foo") + fun longValueClass(limit: LongValueClass) = limit.value @@ -246,6 +255,9 @@ class InvocableHandlerMethodKotlinTests { data class Animal(override val name: String) : Named + @JvmInline + value class StringValueClass(val value: String) + @JvmInline value class LongValueClass(val value: Long) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java index 63f22259942..ce5b9790b26 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java @@ -325,7 +325,7 @@ public class InvocableHandlerMethod extends HandlerMethod { @Nullable @SuppressWarnings({"deprecation", "DataFlowIssue"}) public static Object invokeFunction(Method method, Object target, Object[] args, boolean isSuspendingFunction, - ServerWebExchange exchange) throws InvocationTargetException, IllegalAccessException { + ServerWebExchange exchange) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { if (isSuspendingFunction) { Object coroutineContext = exchange.getAttribute(COROUTINE_CONTEXT_ATTRIBUTE); @@ -369,6 +369,9 @@ public class InvocableHandlerMethod extends HandlerMethod { } } Object result = function.callBy(argMap); + if (result != null && KotlinDetector.isInlineClass(result.getClass())) { + return result.getClass().getDeclaredMethod("unbox-impl").invoke(result); + } return (result == Unit.INSTANCE ? null : result); } } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt index c94cd6bdb81..6f254d473b7 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt @@ -205,6 +205,13 @@ class InvocableHandlerMethodKotlinTests { assertHandlerResultValue(result, "1") } + @Test + fun valueClassReturnValue() { + val method = ValueClassController::valueClassReturnValue.javaMethod!! + val result = invoke(ValueClassController(), method,) + assertHandlerResultValue(result, "foo") + } + @Test fun valueClassWithDefaultValue() { this.resolvers.add(stubResolver(null, Double::class.java)) @@ -376,6 +383,9 @@ class InvocableHandlerMethodKotlinTests { fun valueClass(limit: LongValueClass) = "${limit.value}" + fun valueClassReturnValue() = + StringValueClass("foo") + fun valueClassWithDefault(limit: DoubleValueClass = DoubleValueClass(3.1)) = "${limit.value}" @@ -420,6 +430,9 @@ class InvocableHandlerMethodKotlinTests { data class Animal(override val name: String) : Named + @JvmInline + value class StringValueClass(val value: String) + @JvmInline value class LongValueClass(val value: Long)