Skip to content

Commit 227e75d

Browse files
committed
Support nullable Kotlin value class arguments
This commit refines WebMVC and WebFlux argument resolution in order to convert properly Kotlin value class arguments by using the type of the value instead of the type of the value class. Closes gh-32353
1 parent de828e9 commit 227e75d

File tree

4 files changed

+78
-16
lines changed

4 files changed

+78
-16
lines changed

spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import kotlin.reflect.KParameter;
2727
import kotlin.reflect.jvm.ReflectJvmMapping;
2828

29+
import org.springframework.beans.BeanUtils;
2930
import org.springframework.beans.ConversionNotSupportedException;
3031
import org.springframework.beans.TypeMismatchException;
3132
import org.springframework.beans.factory.config.BeanExpressionContext;
@@ -281,8 +282,12 @@ private static Object convertIfNecessary(
281282
NamedValueInfo namedValueInfo, @Nullable Object arg) throws Exception {
282283

283284
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
285+
Class<?> parameterType = parameter.getParameterType();
286+
if (KotlinDetector.isKotlinPresent() && KotlinDetector.isInlineClass(parameterType)) {
287+
parameterType = BeanUtils.findPrimaryConstructor(parameterType).getParameterTypes()[0];
288+
}
284289
try {
285-
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
290+
arg = binder.convertIfNecessary(arg, parameterType, parameter);
286291
}
287292
catch (ConversionNotSupportedException ex) {
288293
throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),

spring-web/src/test/kotlin/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverKotlinTests.kt

+32-6
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.
@@ -25,7 +25,6 @@ import org.springframework.core.annotation.SynthesizingMethodParameter
2525
import org.springframework.core.convert.support.DefaultConversionService
2626
import org.springframework.http.HttpMethod
2727
import org.springframework.http.MediaType
28-
import org.springframework.util.ReflectionUtils
2928
import org.springframework.web.bind.MissingServletRequestParameterException
3029
import org.springframework.web.bind.annotation.RequestParam
3130
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer
@@ -39,6 +38,7 @@ import org.springframework.web.testfixture.servlet.MockHttpServletRequest
3938
import org.springframework.web.testfixture.servlet.MockHttpServletResponse
4039
import org.springframework.web.testfixture.servlet.MockMultipartFile
4140
import org.springframework.web.testfixture.servlet.MockMultipartHttpServletRequest
41+
import kotlin.reflect.jvm.javaMethod
4242

4343
/**
4444
* Kotlin test fixture for [RequestParamMethodArgumentResolver].
@@ -70,6 +70,9 @@ class RequestParamMethodArgumentResolverKotlinTests {
7070
lateinit var nonNullableMultipartParamRequired: MethodParameter
7171
lateinit var nonNullableMultipartParamNotRequired: MethodParameter
7272

73+
lateinit var nonNullableValueClassParam: MethodParameter
74+
lateinit var nullableValueClassParam: MethodParameter
75+
7376

7477
@BeforeEach
7578
fun setup() {
@@ -80,10 +83,8 @@ class RequestParamMethodArgumentResolverKotlinTests {
8083
binderFactory = DefaultDataBinderFactory(initializer)
8184
webRequest = ServletWebRequest(request, MockHttpServletResponse())
8285

83-
val method = ReflectionUtils.findMethod(javaClass, "handle",
84-
String::class.java, String::class.java, String::class.java, String::class.java,
85-
Boolean::class.java, Boolean::class.java, Int::class.java, Int::class.java, String::class.java, String::class.java,
86-
MultipartFile::class.java, MultipartFile::class.java, MultipartFile::class.java, MultipartFile::class.java)!!
86+
val method = RequestParamMethodArgumentResolverKotlinTests::handle.javaMethod!!
87+
val valueClassMethod = RequestParamMethodArgumentResolverKotlinTests::handleValueClass.javaMethod!!
8788

8889
nullableParamRequired = SynthesizingMethodParameter(method, 0)
8990
nullableParamNotRequired = SynthesizingMethodParameter(method, 1)
@@ -101,6 +102,9 @@ class RequestParamMethodArgumentResolverKotlinTests {
101102
nullableMultipartParamNotRequired = SynthesizingMethodParameter(method, 11)
102103
nonNullableMultipartParamRequired = SynthesizingMethodParameter(method, 12)
103104
nonNullableMultipartParamNotRequired = SynthesizingMethodParameter(method, 13)
105+
106+
nonNullableValueClassParam = SynthesizingMethodParameter(valueClassMethod, 0)
107+
nullableValueClassParam = SynthesizingMethodParameter(valueClassMethod, 1)
104108
}
105109

106110
@Test
@@ -317,6 +321,20 @@ class RequestParamMethodArgumentResolverKotlinTests {
317321
}
318322
}
319323

324+
@Test
325+
fun resolveNonNullableValueClass() {
326+
request.addParameter("value", "123")
327+
var result = resolver.resolveArgument(nonNullableValueClassParam, null, webRequest, binderFactory)
328+
assertThat(result).isEqualTo(123)
329+
}
330+
331+
@Test
332+
fun resolveNullableValueClass() {
333+
request.addParameter("value", "123")
334+
var result = resolver.resolveArgument(nullableValueClassParam, null, webRequest, binderFactory)
335+
assertThat(result).isEqualTo(123)
336+
}
337+
320338

321339
@Suppress("unused_parameter")
322340
fun handle(
@@ -338,5 +356,13 @@ class RequestParamMethodArgumentResolverKotlinTests {
338356
@RequestParam("mfile", required = false) nonNullableMultipartParamNotRequired: MultipartFile) {
339357
}
340358

359+
@Suppress("unused_parameter")
360+
fun handleValueClass(
361+
@RequestParam("value") nonNullable: ValueClass,
362+
@RequestParam("value") nullable: ValueClass?) {
363+
}
364+
365+
@JvmInline value class ValueClass(val value: Int)
366+
341367
}
342368

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import kotlin.reflect.jvm.ReflectJvmMapping;
2727
import reactor.core.publisher.Mono;
2828

29+
import org.springframework.beans.BeanUtils;
2930
import org.springframework.beans.ConversionNotSupportedException;
3031
import org.springframework.beans.TypeMismatchException;
3132
import org.springframework.beans.factory.config.BeanExpressionContext;
@@ -197,8 +198,12 @@ private Object applyConversion(@Nullable Object value, NamedValueInfo namedValue
197198
BindingContext bindingContext, ServerWebExchange exchange) {
198199

199200
WebDataBinder binder = bindingContext.createDataBinder(exchange, namedValueInfo.name);
201+
Class<?> parameterType = parameter.getParameterType();
202+
if (KotlinDetector.isKotlinPresent() && KotlinDetector.isInlineClass(parameterType)) {
203+
parameterType = BeanUtils.findPrimaryConstructor(parameterType).getParameterTypes()[0];
204+
}
200205
try {
201-
value = binder.convertIfNecessary(value, parameter.getParameterType(), parameter);
206+
value = binder.convertIfNecessary(value, parameterType, parameter);
202207
}
203208
catch (ConversionNotSupportedException ex) {
204209
throw new ServerErrorException("Conversion not supported.", parameter, ex);

spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverKotlinTests.kt

+34-8
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.
@@ -22,14 +22,14 @@ import org.springframework.core.MethodParameter
2222
import org.springframework.core.ReactiveAdapterRegistry
2323
import org.springframework.core.annotation.SynthesizingMethodParameter
2424
import org.springframework.format.support.DefaultFormattingConversionService
25-
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest
26-
import org.springframework.web.testfixture.server.MockServerWebExchange
27-
import org.springframework.util.ReflectionUtils
2825
import org.springframework.web.bind.annotation.RequestParam
2926
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer
3027
import org.springframework.web.reactive.BindingContext
3128
import org.springframework.web.server.ServerWebInputException
29+
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest
30+
import org.springframework.web.testfixture.server.MockServerWebExchange
3231
import reactor.test.StepVerifier
32+
import kotlin.reflect.jvm.javaMethod
3333

3434
/**
3535
* Kotlin test fixture for [RequestParamMethodArgumentResolver].
@@ -53,6 +53,9 @@ class RequestParamMethodArgumentResolverKotlinTests {
5353
lateinit var defaultValueStringParamRequired: MethodParameter
5454
lateinit var defaultValueStringParamNotRequired: MethodParameter
5555

56+
lateinit var nonNullableValueClassParam: MethodParameter
57+
lateinit var nullableValueClassParam: MethodParameter
58+
5659

5760
@BeforeEach
5861
fun setup() {
@@ -61,10 +64,8 @@ class RequestParamMethodArgumentResolverKotlinTests {
6164
initializer.conversionService = DefaultFormattingConversionService()
6265
bindingContext = BindingContext(initializer)
6366

64-
val method = ReflectionUtils.findMethod(javaClass, "handle",
65-
String::class.java, String::class.java, String::class.java, String::class.java,
66-
Boolean::class.java, Boolean::class.java, Int::class.java, Int::class.java,
67-
String::class.java, String::class.java)!!
67+
val method = RequestParamMethodArgumentResolverKotlinTests::handle.javaMethod!!
68+
val valueClassMethod = RequestParamMethodArgumentResolverKotlinTests::handleValueClass.javaMethod!!
6869

6970
nullableParamRequired = SynthesizingMethodParameter(method, 0)
7071
nullableParamNotRequired = SynthesizingMethodParameter(method, 1)
@@ -77,6 +78,9 @@ class RequestParamMethodArgumentResolverKotlinTests {
7778
defaultValueIntParamNotRequired = SynthesizingMethodParameter(method, 7)
7879
defaultValueStringParamRequired = SynthesizingMethodParameter(method, 8)
7980
defaultValueStringParamNotRequired = SynthesizingMethodParameter(method, 9)
81+
82+
nonNullableValueClassParam = SynthesizingMethodParameter(valueClassMethod, 0)
83+
nullableValueClassParam = SynthesizingMethodParameter(valueClassMethod, 1)
8084
}
8185

8286
@Test
@@ -219,6 +223,20 @@ class RequestParamMethodArgumentResolverKotlinTests {
219223
StepVerifier.create(result).expectComplete().verify()
220224
}
221225

226+
@Test
227+
fun resolveNonNullableValueClass() {
228+
var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path?value=123"))
229+
var result = resolver.resolveArgument(nonNullableValueClassParam, bindingContext, exchange)
230+
StepVerifier.create(result).expectNext(123).expectComplete().verify()
231+
}
232+
233+
@Test
234+
fun resolveNullableValueClass() {
235+
var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path?value=123"))
236+
var result = resolver.resolveArgument(nullableValueClassParam, bindingContext, exchange)
237+
StepVerifier.create(result).expectNext(123).expectComplete().verify()
238+
}
239+
222240

223241
@Suppress("unused_parameter")
224242
fun handle(
@@ -235,5 +253,13 @@ class RequestParamMethodArgumentResolverKotlinTests {
235253
@RequestParam("value", required = false) withDefaultValueStringParamNotRequired: String = "default") {
236254
}
237255

256+
@Suppress("unused_parameter")
257+
fun handleValueClass(
258+
@RequestParam("value") nonNullable: ValueClass,
259+
@RequestParam("value") nullable: ValueClass?) {
260+
}
261+
262+
@JvmInline value class ValueClass(val value: Int)
263+
238264
}
239265

0 commit comments

Comments
 (0)