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
This commit is contained in:
Sébastien Deleuze 2024-07-10 18:31:35 +02:00
parent 82c5aa4a48
commit 7617a01f60
6 changed files with 67 additions and 3 deletions

View File

@ -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<Object> 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();
}
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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)