From dcdfbc3940e747c09621dadbfcd181221b7fd431 Mon Sep 17 00:00:00 2001 From: Dmitry Sulman Date: Thu, 13 Mar 2025 14:57:57 +0200 Subject: [PATCH] Recursively boxing/unboxing nested inline value classes See spring-projectsgh-34458 Signed-off-by: Dmitry Sulman --- .../support/InvocableHandlerMethod.java | 25 ++++++++++++----- .../InvocableHandlerMethodKotlinTests.kt | 25 ++++++++++++++++- .../result/method/InvocableHandlerMethod.java | 25 ++++++++++++----- .../InvocableHandlerMethodKotlinTests.kt | 27 ++++++++++++++++++- 4 files changed, 88 insertions(+), 14 deletions(-) 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 8bfdbdb7f024..64266224f31b 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 @@ -318,11 +318,7 @@ private static class KotlinDelegate { KType type = parameter.getType(); if (!(type.isMarkedNullable() && arg == null) && type.getClassifier() instanceof KClass kClass && KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) { - KFunction constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass)); - if (!KCallablesJvm.isAccessible(constructor)) { - KCallablesJvm.setAccessible(constructor, true); - } - arg = constructor.call(arg); + arg = box(kClass, arg); } argMap.put(parameter, arg); } @@ -337,6 +333,19 @@ private static class KotlinDelegate { return (result == Unit.INSTANCE ? null : result); } + private static Object box(KClass kClass, @Nullable Object arg) { + KFunction constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass)); + KType type = constructor.getParameters().get(0).getType(); + if (!(type.isMarkedNullable() && arg == null) && type.getClassifier() instanceof KClass parameterClass + && KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) { + arg = box(parameterClass, arg); + } + if (!KCallablesJvm.isAccessible(constructor)) { + KCallablesJvm.setAccessible(constructor, true); + } + return constructor.call(arg); + } + private static void handleResult(Object result, SynchronousSink sink) { if (KotlinDetector.isInlineClass(result.getClass())) { try { @@ -357,7 +366,11 @@ private static void handleResult(Object result, SynchronousSink sink) { } private static Object unbox(Object result) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { - return result.getClass().getDeclaredMethod("unbox-impl").invoke(result); + Object unboxed = result.getClass().getDeclaredMethod("unbox-impl").invoke(result); + if (KotlinDetector.isInlineClass(unboxed.getClass())) { + return unbox(unboxed); + } + return unboxed; } } 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 c1f63e8b30cc..a47f54f4ba3d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -109,12 +109,25 @@ class InvocableHandlerMethodKotlinTests { Assertions.assertThat(value).isEqualTo(1L) } + @Test + fun nestedValueClass() { + composite.addResolver(StubArgumentResolver(Long::class.java, 1L)) + val value = getInvocable(ValueClassHandler::nestedValueClass.javaMethod!!).invokeForRequest(request, null) + Assertions.assertThat(value).isEqualTo(1L) + } + @Test fun valueClassReturnValue() { val value = getInvocable(ValueClassHandler::valueClassReturnValue.javaMethod!!).invokeForRequest(request, null) Assertions.assertThat(value).isEqualTo("foo") } + @Test + fun nestedValueClassReturnValue() { + val value = getInvocable(ValueClassHandler::nestedValueClassReturnValue.javaMethod!!).invokeForRequest(request, null) + Assertions.assertThat(value).isEqualTo("foo") + } + @Test fun resultOfUnitReturnValue() { val value = getInvocable(ValueClassHandler::resultOfUnitReturnValue.javaMethod!!).invokeForRequest(request, null) @@ -273,10 +286,14 @@ class InvocableHandlerMethodKotlinTests { fun valueClassReturnValue() = StringValueClass("foo") + fun nestedValueClassReturnValue() = NestedStringValueClass(StringValueClass("foo")) + fun resultOfUnitReturnValue() = Result.success(Unit) fun longValueClass(limit: LongValueClass) = limit.value + fun nestedValueClass(limit: ULongValueClass) = limit.value + fun doubleValueClass(limit: DoubleValueClass = DoubleValueClass(3.1)) = limit.value fun valueClassWithInit(valueClass: ValueClassWithInit) = valueClass @@ -358,9 +375,15 @@ class InvocableHandlerMethodKotlinTests { @JvmInline value class StringValueClass(val value: String) + @JvmInline + value class NestedStringValueClass(val value: StringValueClass) + @JvmInline value class LongValueClass(val value: Long) + @JvmInline + value class ULongValueClass(val value: ULong) + @JvmInline value class DoubleValueClass(val value: Double) 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 59d6a848aa12..2c11fddcc01d 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 @@ -356,11 +356,7 @@ private static class KotlinDelegate { KType type = parameter.getType(); if (!(type.isMarkedNullable() && arg == null) && type.getClassifier() instanceof KClass kClass && KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) { - KFunction constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass)); - if (!KCallablesJvm.isAccessible(constructor)) { - KCallablesJvm.setAccessible(constructor, true); - } - arg = constructor.call(arg); + arg = box(kClass, arg); } argMap.put(parameter, arg); } @@ -376,6 +372,19 @@ private static class KotlinDelegate { } } + private static Object box(KClass kClass, Object arg) { + KFunction constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass)); + KType type = constructor.getParameters().get(0).getType(); + if (!(type.isMarkedNullable() && arg == null) && type.getClassifier() instanceof KClass parameterClass + && KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) { + arg = box(parameterClass, arg); + } + if (!KCallablesJvm.isAccessible(constructor)) { + KCallablesJvm.setAccessible(constructor, true); + } + return constructor.call(arg); + } + private static void handleResult(Object result, SynchronousSink sink) { if (KotlinDetector.isInlineClass(result.getClass())) { try { @@ -396,7 +405,11 @@ private static void handleResult(Object result, SynchronousSink sink) { } private static Object unbox(Object result) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { - return result.getClass().getDeclaredMethod("unbox-impl").invoke(result); + Object unboxed = result.getClass().getDeclaredMethod("unbox-impl").invoke(result); + if (KotlinDetector.isInlineClass(unboxed.getClass())) { + return unbox(unboxed); + } + return unboxed; } } 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 595928ec134e..cfdc3f17068f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -205,6 +205,14 @@ class InvocableHandlerMethodKotlinTests { assertHandlerResultValue(result, "1") } + @Test + fun nestedValueClass() { + this.resolvers.add(stubResolver(1L, Long::class.java)) + val method = ValueClassController::nestedValueClass.javaMethod!! + val result = invoke(ValueClassController(), method,1L) + assertHandlerResultValue(result, "1") + } + @Test fun valueClassReturnValue() { val method = ValueClassController::valueClassReturnValue.javaMethod!! @@ -212,6 +220,13 @@ class InvocableHandlerMethodKotlinTests { assertHandlerResultValue(result, "foo") } + @Test + fun nestedValueClassReturnValue() { + val method = ValueClassController::nestedValueClassReturnValue.javaMethod!! + val result = invoke(ValueClassController(), method) + assertHandlerResultValue(result, "foo") + } + @Test fun resultOfUnitReturnValue() { val method = ValueClassController::resultOfUnitReturnValue.javaMethod!! @@ -448,8 +463,12 @@ class InvocableHandlerMethodKotlinTests { fun valueClass(limit: LongValueClass) = "${limit.value}" + fun nestedValueClass(limit: ULongValueClass) = "${limit.value}" + fun valueClassReturnValue() = StringValueClass("foo") + fun nestedValueClassReturnValue() = NestedStringValueClass(StringValueClass("foo")) + fun resultOfUnitReturnValue() = Result.success(Unit) fun valueClassWithDefault(limit: DoubleValueClass = DoubleValueClass(3.1)) = "${limit.value}" @@ -533,9 +552,15 @@ class InvocableHandlerMethodKotlinTests { @JvmInline value class StringValueClass(val value: String) + @JvmInline + value class NestedStringValueClass(val value: StringValueClass) + @JvmInline value class LongValueClass(val value: Long) + @JvmInline + value class ULongValueClass(val value: ULong) + @JvmInline value class DoubleValueClass(val value: Double)