Skip to content

Commit 516a203

Browse files
committed
Support nullable Kotlin value class arguments
This commit skips the value class parameter instantiation for nullable types when a null argument is passed. Closes spring-projectsgh-32353
1 parent 877e0b1 commit 516a203

File tree

6 files changed

+86
-20
lines changed

6 files changed

+86
-20
lines changed

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -115,18 +115,20 @@ public static Publisher<?> invokeSuspendingFunction(CoroutineContext context, Me
115115
switch (parameter.getKind()) {
116116
case INSTANCE -> argMap.put(parameter, target);
117117
case VALUE, EXTENSION_RECEIVER -> {
118-
if (!parameter.isOptional() || args[index] != null) {
118+
Object arg = args[index];
119+
if (!(parameter.isOptional() && arg == null)) {
119120
if (parameter.getType().getClassifier() instanceof KClass<?> kClass) {
120121
Class<?> javaClass = JvmClassMappingKt.getJavaClass(kClass);
121-
if (KotlinDetector.isInlineClass(javaClass)) {
122-
argMap.put(parameter, KClasses.getPrimaryConstructor(kClass).call(args[index]));
122+
if (KotlinDetector.isInlineClass(javaClass)
123+
&& !(parameter.getType().isMarkedNullable() && arg == null)) {
124+
argMap.put(parameter, KClasses.getPrimaryConstructor(kClass).call(arg));
123125
}
124126
else {
125-
argMap.put(parameter, args[index]);
127+
argMap.put(parameter, arg);
126128
}
127129
}
128130
else {
129-
argMap.put(parameter, args[index]);
131+
argMap.put(parameter, arg);
130132
}
131133
}
132134
index++;

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

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 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.
@@ -82,6 +82,15 @@ class CoroutinesUtilsTests {
8282
.verify()
8383
}
8484

85+
@Test
86+
fun invokeSuspendingFunctionWithNullableParameter() {
87+
val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingFunctionWithNullable", String::class.java, Continuation::class.java)
88+
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, null, null) as Mono
89+
runBlocking {
90+
Assertions.assertThat(mono.awaitSingleOrNull()).isNull()
91+
}
92+
}
93+
8594
@Test
8695
fun invokeNonSuspendingFunction() {
8796
val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("nonSuspendingFunction", String::class.java)
@@ -165,6 +174,15 @@ class CoroutinesUtilsTests {
165174
}
166175
}
167176

177+
@Test
178+
fun invokeSuspendingFunctionWithNullableValueClassParameter() {
179+
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithNullableValueClass") }
180+
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, null, null) as Mono
181+
runBlocking {
182+
Assertions.assertThat(mono.awaitSingleOrNull()).isNull()
183+
}
184+
}
185+
168186
@Test
169187
fun invokeSuspendingFunctionWithExtension() {
170188
val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingFunctionWithExtension",
@@ -190,6 +208,11 @@ class CoroutinesUtilsTests {
190208
return value
191209
}
192210

211+
suspend fun suspendingFunctionWithNullable(value: String?): String? {
212+
delay(1)
213+
return value
214+
}
215+
193216
suspend fun suspendingFunctionWithFlow(): Flow<String> {
194217
delay(1)
195218
return flowOf("foo", "bar")
@@ -222,6 +245,11 @@ class CoroutinesUtilsTests {
222245
return value.value
223246
}
224247

248+
suspend fun suspendingFunctionWithNullableValueClass(value: ValueClass?): String? {
249+
delay(1)
250+
return value?.value
251+
}
252+
225253
suspend fun CustomException.suspendingFunctionWithExtension(): String {
226254
delay(1)
227255
return "${this.message}"

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -313,18 +313,20 @@ public static Object invokeFunction(Method method, Object target, Object[] args)
313313
switch (parameter.getKind()) {
314314
case INSTANCE -> argMap.put(parameter, target);
315315
case VALUE, EXTENSION_RECEIVER -> {
316-
if (!parameter.isOptional() || args[index] != null) {
316+
Object arg = args[index];
317+
if (!(parameter.isOptional() && arg == null)) {
317318
if (parameter.getType().getClassifier() instanceof KClass<?> kClass) {
318319
Class<?> javaClass = JvmClassMappingKt.getJavaClass(kClass);
319-
if (KotlinDetector.isInlineClass(javaClass)) {
320-
argMap.put(parameter, KClasses.getPrimaryConstructor(kClass).call(args[index]));
320+
if (KotlinDetector.isInlineClass(javaClass)
321+
&& !(parameter.getType().isMarkedNullable() && arg == null)) {
322+
argMap.put(parameter, KClasses.getPrimaryConstructor(kClass).call(arg));
321323
}
322324
else {
323-
argMap.put(parameter, args[index]);
325+
argMap.put(parameter, arg);
324326
}
325327
}
326328
else {
327-
argMap.put(parameter, args[index]);
329+
argMap.put(parameter, arg);
328330
}
329331
}
330332
index++;

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

+10
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ class InvocableHandlerMethodKotlinTests {
105105
Assertions.assertThatIllegalArgumentException().isThrownBy { invocable.invokeForRequest(request, null) }
106106
}
107107

108+
@Test
109+
fun valueClassWithNullable() {
110+
composite.addResolver(StubArgumentResolver(LongValueClass::class.java, null))
111+
val value = getInvocable(ValueClassHandler::class.java, LongValueClass::class.java).invokeForRequest(request, null)
112+
Assertions.assertThat(value).isNull()
113+
}
114+
108115
@Test
109116
fun propertyAccessor() {
110117
val value = getInvocable(PropertyAccessorHandler::class.java).invokeForRequest(request, null)
@@ -173,6 +180,9 @@ class InvocableHandlerMethodKotlinTests {
173180
fun valueClassWithInit(valueClass: ValueClassWithInit) =
174181
valueClass
175182

183+
fun valueClassWithNullable(limit: LongValueClass?) =
184+
limit?.value
185+
176186
}
177187

178188
private class PropertyAccessorHandler {

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -324,18 +324,20 @@ public static Object invokeFunction(Method method, Object target, Object[] args,
324324
switch (parameter.getKind()) {
325325
case INSTANCE -> argMap.put(parameter, target);
326326
case VALUE, EXTENSION_RECEIVER -> {
327-
if (!parameter.isOptional() || args[index] != null) {
327+
Object arg = args[index];
328+
if (!(parameter.isOptional() && arg == null)) {
328329
if (parameter.getType().getClassifier() instanceof KClass<?> kClass) {
329330
Class<?> javaClass = JvmClassMappingKt.getJavaClass(kClass);
330-
if (KotlinDetector.isInlineClass(javaClass)) {
331-
argMap.put(parameter, KClasses.getPrimaryConstructor(kClass).call(args[index]));
331+
if (KotlinDetector.isInlineClass(javaClass)
332+
&& !(parameter.getType().isMarkedNullable() && arg == null)) {
333+
argMap.put(parameter, KClasses.getPrimaryConstructor(kClass).call(arg));
332334
}
333335
else {
334-
argMap.put(parameter, args[index]);
336+
argMap.put(parameter, arg);
335337
}
336338
}
337339
else {
338-
argMap.put(parameter, args[index]);
340+
argMap.put(parameter, arg);
339341
}
340342
}
341343
index++;

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

+26-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package org.springframework.web.reactive.result
1919
import io.mockk.every
2020
import io.mockk.mockk
2121
import kotlinx.coroutines.delay
22-
import org.assertj.core.api.Assertions
2322
import org.assertj.core.api.Assertions.assertThat
2423
import org.junit.jupiter.api.Test
2524
import org.springframework.core.MethodParameter
@@ -178,11 +177,19 @@ class InvocableHandlerMethodKotlinTests {
178177

179178
@Test
180179
fun nullReturnValue() {
181-
val method = NullResultController::nullable.javaMethod!!
180+
val method = NullResultController::nullableReturnValue.javaMethod!!
182181
val result = invoke(NullResultController(), method)
183182
assertHandlerResultValue(result, null)
184183
}
185184

185+
@Test
186+
fun nullParameter() {
187+
this.resolvers.add(stubResolver(null, String::class.java))
188+
val method = NullResultController::nullableParameter.javaMethod!!
189+
val result = invoke(NullResultController(), method, null)
190+
assertHandlerResultValue(result, null)
191+
}
192+
186193
@Test
187194
fun valueClass() {
188195
this.resolvers.add(stubResolver(1L, Long::class.java))
@@ -192,7 +199,7 @@ class InvocableHandlerMethodKotlinTests {
192199
}
193200

194201
@Test
195-
fun valueClassDefaultValue() {
202+
fun valueClassWithDefaultValue() {
196203
this.resolvers.add(stubResolver(null, Double::class.java))
197204
val method = ValueClassController::valueClassWithDefault.javaMethod!!
198205
val result = invoke(ValueClassController(), method)
@@ -207,6 +214,14 @@ class InvocableHandlerMethodKotlinTests {
207214
assertExceptionThrown(result, IllegalArgumentException::class)
208215
}
209216

217+
@Test
218+
fun valueClassWithNullable() {
219+
this.resolvers.add(stubResolver(null, LongValueClass::class.java))
220+
val method = ValueClassController::valueClassWithNullable.javaMethod!!
221+
val result = invoke(ValueClassController(), method, null)
222+
assertHandlerResultValue(result, "null")
223+
}
224+
210225
@Test
211226
fun propertyAccessor() {
212227
this.resolvers.add(stubResolver(null, String::class.java))
@@ -321,9 +336,13 @@ class InvocableHandlerMethodKotlinTests {
321336
fun unit() {
322337
}
323338

324-
fun nullable(): String? {
339+
fun nullableReturnValue(): String? {
325340
return null
326341
}
342+
343+
fun nullableParameter(value: String?): String? {
344+
return value
345+
}
327346
}
328347

329348
class ValueClassController {
@@ -337,6 +356,9 @@ class InvocableHandlerMethodKotlinTests {
337356
fun valueClassWithInit(valueClass: ValueClassWithInit) =
338357
valueClass
339358

359+
fun valueClassWithNullable(limit: LongValueClass?) =
360+
"${limit?.value}"
361+
340362
}
341363

342364
class PropertyAccessorController {

0 commit comments

Comments
 (0)