Skip to content

Commit dcdfbc3

Browse files
committed
Recursively boxing/unboxing nested inline value classes
See spring-projectsgh-34458 Signed-off-by: Dmitry Sulman <[email protected]>
1 parent 90e32ca commit dcdfbc3

File tree

4 files changed

+88
-14
lines changed

4 files changed

+88
-14
lines changed

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

+19-6
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,7 @@ private static class KotlinDelegate {
318318
KType type = parameter.getType();
319319
if (!(type.isMarkedNullable() && arg == null) && type.getClassifier() instanceof KClass<?> kClass
320320
&& KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
321-
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
322-
if (!KCallablesJvm.isAccessible(constructor)) {
323-
KCallablesJvm.setAccessible(constructor, true);
324-
}
325-
arg = constructor.call(arg);
321+
arg = box(kClass, arg);
326322
}
327323
argMap.put(parameter, arg);
328324
}
@@ -337,6 +333,19 @@ private static class KotlinDelegate {
337333
return (result == Unit.INSTANCE ? null : result);
338334
}
339335

336+
private static Object box(KClass<?> kClass, @Nullable Object arg) {
337+
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
338+
KType type = constructor.getParameters().get(0).getType();
339+
if (!(type.isMarkedNullable() && arg == null) && type.getClassifier() instanceof KClass<?> parameterClass
340+
&& KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
341+
arg = box(parameterClass, arg);
342+
}
343+
if (!KCallablesJvm.isAccessible(constructor)) {
344+
KCallablesJvm.setAccessible(constructor, true);
345+
}
346+
return constructor.call(arg);
347+
}
348+
340349
private static void handleResult(Object result, SynchronousSink<Object> sink) {
341350
if (KotlinDetector.isInlineClass(result.getClass())) {
342351
try {
@@ -357,7 +366,11 @@ private static void handleResult(Object result, SynchronousSink<Object> sink) {
357366
}
358367

359368
private static Object unbox(Object result) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
360-
return result.getClass().getDeclaredMethod("unbox-impl").invoke(result);
369+
Object unboxed = result.getClass().getDeclaredMethod("unbox-impl").invoke(result);
370+
if (KotlinDetector.isInlineClass(unboxed.getClass())) {
371+
return unbox(unboxed);
372+
}
373+
return unboxed;
361374
}
362375
}
363376

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

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -109,12 +109,25 @@ class InvocableHandlerMethodKotlinTests {
109109
Assertions.assertThat(value).isEqualTo(1L)
110110
}
111111

112+
@Test
113+
fun nestedValueClass() {
114+
composite.addResolver(StubArgumentResolver(Long::class.java, 1L))
115+
val value = getInvocable(ValueClassHandler::nestedValueClass.javaMethod!!).invokeForRequest(request, null)
116+
Assertions.assertThat(value).isEqualTo(1L)
117+
}
118+
112119
@Test
113120
fun valueClassReturnValue() {
114121
val value = getInvocable(ValueClassHandler::valueClassReturnValue.javaMethod!!).invokeForRequest(request, null)
115122
Assertions.assertThat(value).isEqualTo("foo")
116123
}
117124

125+
@Test
126+
fun nestedValueClassReturnValue() {
127+
val value = getInvocable(ValueClassHandler::nestedValueClassReturnValue.javaMethod!!).invokeForRequest(request, null)
128+
Assertions.assertThat(value).isEqualTo("foo")
129+
}
130+
118131
@Test
119132
fun resultOfUnitReturnValue() {
120133
val value = getInvocable(ValueClassHandler::resultOfUnitReturnValue.javaMethod!!).invokeForRequest(request, null)
@@ -273,10 +286,14 @@ class InvocableHandlerMethodKotlinTests {
273286

274287
fun valueClassReturnValue() = StringValueClass("foo")
275288

289+
fun nestedValueClassReturnValue() = NestedStringValueClass(StringValueClass("foo"))
290+
276291
fun resultOfUnitReturnValue() = Result.success(Unit)
277292

278293
fun longValueClass(limit: LongValueClass) = limit.value
279294

295+
fun nestedValueClass(limit: ULongValueClass) = limit.value
296+
280297
fun doubleValueClass(limit: DoubleValueClass = DoubleValueClass(3.1)) = limit.value
281298

282299
fun valueClassWithInit(valueClass: ValueClassWithInit) = valueClass
@@ -358,9 +375,15 @@ class InvocableHandlerMethodKotlinTests {
358375
@JvmInline
359376
value class StringValueClass(val value: String)
360377

378+
@JvmInline
379+
value class NestedStringValueClass(val value: StringValueClass)
380+
361381
@JvmInline
362382
value class LongValueClass(val value: Long)
363383

384+
@JvmInline
385+
value class ULongValueClass(val value: ULong)
386+
364387
@JvmInline
365388
value class DoubleValueClass(val value: Double)
366389

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

+19-6
Original file line numberDiff line numberDiff line change
@@ -356,11 +356,7 @@ private static class KotlinDelegate {
356356
KType type = parameter.getType();
357357
if (!(type.isMarkedNullable() && arg == null) && type.getClassifier() instanceof KClass<?> kClass
358358
&& KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
359-
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
360-
if (!KCallablesJvm.isAccessible(constructor)) {
361-
KCallablesJvm.setAccessible(constructor, true);
362-
}
363-
arg = constructor.call(arg);
359+
arg = box(kClass, arg);
364360
}
365361
argMap.put(parameter, arg);
366362
}
@@ -376,6 +372,19 @@ private static class KotlinDelegate {
376372
}
377373
}
378374

375+
private static Object box(KClass<?> kClass, Object arg) {
376+
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
377+
KType type = constructor.getParameters().get(0).getType();
378+
if (!(type.isMarkedNullable() && arg == null) && type.getClassifier() instanceof KClass<?> parameterClass
379+
&& KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
380+
arg = box(parameterClass, arg);
381+
}
382+
if (!KCallablesJvm.isAccessible(constructor)) {
383+
KCallablesJvm.setAccessible(constructor, true);
384+
}
385+
return constructor.call(arg);
386+
}
387+
379388
private static void handleResult(Object result, SynchronousSink<Object> sink) {
380389
if (KotlinDetector.isInlineClass(result.getClass())) {
381390
try {
@@ -396,7 +405,11 @@ private static void handleResult(Object result, SynchronousSink<Object> sink) {
396405
}
397406

398407
private static Object unbox(Object result) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
399-
return result.getClass().getDeclaredMethod("unbox-impl").invoke(result);
408+
Object unboxed = result.getClass().getDeclaredMethod("unbox-impl").invoke(result);
409+
if (KotlinDetector.isInlineClass(unboxed.getClass())) {
410+
return unbox(unboxed);
411+
}
412+
return unboxed;
400413
}
401414

402415
}

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

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -205,13 +205,28 @@ class InvocableHandlerMethodKotlinTests {
205205
assertHandlerResultValue(result, "1")
206206
}
207207

208+
@Test
209+
fun nestedValueClass() {
210+
this.resolvers.add(stubResolver(1L, Long::class.java))
211+
val method = ValueClassController::nestedValueClass.javaMethod!!
212+
val result = invoke(ValueClassController(), method,1L)
213+
assertHandlerResultValue(result, "1")
214+
}
215+
208216
@Test
209217
fun valueClassReturnValue() {
210218
val method = ValueClassController::valueClassReturnValue.javaMethod!!
211219
val result = invoke(ValueClassController(), method)
212220
assertHandlerResultValue(result, "foo")
213221
}
214222

223+
@Test
224+
fun nestedValueClassReturnValue() {
225+
val method = ValueClassController::nestedValueClassReturnValue.javaMethod!!
226+
val result = invoke(ValueClassController(), method)
227+
assertHandlerResultValue(result, "foo")
228+
}
229+
215230
@Test
216231
fun resultOfUnitReturnValue() {
217232
val method = ValueClassController::resultOfUnitReturnValue.javaMethod!!
@@ -448,8 +463,12 @@ class InvocableHandlerMethodKotlinTests {
448463

449464
fun valueClass(limit: LongValueClass) = "${limit.value}"
450465

466+
fun nestedValueClass(limit: ULongValueClass) = "${limit.value}"
467+
451468
fun valueClassReturnValue() = StringValueClass("foo")
452469

470+
fun nestedValueClassReturnValue() = NestedStringValueClass(StringValueClass("foo"))
471+
453472
fun resultOfUnitReturnValue() = Result.success(Unit)
454473

455474
fun valueClassWithDefault(limit: DoubleValueClass = DoubleValueClass(3.1)) = "${limit.value}"
@@ -533,9 +552,15 @@ class InvocableHandlerMethodKotlinTests {
533552
@JvmInline
534553
value class StringValueClass(val value: String)
535554

555+
@JvmInline
556+
value class NestedStringValueClass(val value: StringValueClass)
557+
536558
@JvmInline
537559
value class LongValueClass(val value: Long)
538560

561+
@JvmInline
562+
value class ULongValueClass(val value: ULong)
563+
539564
@JvmInline
540565
value class DoubleValueClass(val value: Double)
541566

0 commit comments

Comments
 (0)