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 39a7c91824..2b5b0e8c57 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 @@ -18,11 +18,14 @@ package org.springframework.web.method.support; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Map; import java.util.Objects; import kotlin.Unit; +import kotlin.jvm.JvmClassMappingKt; +import kotlin.reflect.KClass; import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; import kotlin.reflect.jvm.KCallablesJvm; @@ -35,8 +38,10 @@ import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.validation.method.MethodValidator; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.support.SessionStatus; @@ -60,6 +65,9 @@ public class InvocableHandlerMethod extends HandlerMethod { private static final Class[] EMPTY_GROUPS = new Class[0]; + private static final ReflectionUtils.MethodFilter boxImplFilter = + (method -> method.isSynthetic() && Modifier.isStatic(method.getModifiers()) && method.getName().equals("box-impl")); + private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); @@ -310,7 +318,15 @@ public class InvocableHandlerMethod extends HandlerMethod { case INSTANCE -> argMap.put(parameter, target); case VALUE -> { if (!parameter.isOptional() || args[index] != null) { - argMap.put(parameter, args[index]); + if (parameter.getType().getClassifier() instanceof KClass kClass && kClass.isValue()) { + Class javaClass = JvmClassMappingKt.getJavaClass(kClass); + Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter); + Assert.state(methods.length == 1, "Unable to find a single box-impl synthetic static method in " + javaClass.getName()); + argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index])); + } + else { + argMap.put(parameter, args[index]); + } } index++; } 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 24549b56c6..6eb2b362d5 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 @@ -84,6 +84,20 @@ class InvocableHandlerMethodKotlinTests { Assertions.assertThat(value).isNull() } + @Test + fun valueClass() { + composite.addResolver(StubArgumentResolver(Long::class.java, 1L)) + val value = getInvocable(Long::class.java).invokeForRequest(request, null) + Assertions.assertThat(value).isEqualTo(1L) + } + + @Test + fun valueClassDefaultValue() { + composite.addResolver(StubArgumentResolver(Double::class.java)) + val value = getInvocable(Double::class.java).invokeForRequest(request, null) + Assertions.assertThat(value).isEqualTo(3.1) + } + private fun getInvocable(vararg argTypes: Class<*>): InvocableHandlerMethod { val method = ResolvableMethod.on(Handler::class.java).argTypes(*argTypes).resolveMethod() val handlerMethod = InvocableHandlerMethod(Handler(), method) @@ -116,6 +130,18 @@ class InvocableHandlerMethodKotlinTests { fun nullable(arg: String?): String? { return null } + + fun valueClass(limit: LongValueClass) = + limit.value + + fun valueClass(limit: DoubleValueClass = DoubleValueClass(3.1)) = + limit.value } + @JvmInline + value class LongValueClass(val value: Long) + + @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 466adbcf1b..b52a3b97da 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 @@ -18,6 +18,7 @@ package org.springframework.web.reactive.result.method; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; @@ -28,6 +29,8 @@ import java.util.stream.Stream; import kotlin.Unit; import kotlin.coroutines.CoroutineContext; +import kotlin.jvm.JvmClassMappingKt; +import kotlin.reflect.KClass; import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; import kotlin.reflect.jvm.KCallablesJvm; @@ -44,8 +47,10 @@ import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.http.HttpStatusCode; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.validation.method.MethodValidator; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.BindingContext; @@ -70,6 +75,9 @@ public class InvocableHandlerMethod extends HandlerMethod { private static final Object NO_ARG_VALUE = new Object(); + private static final ReflectionUtils.MethodFilter boxImplFilter = + (method -> method.isSynthetic() && Modifier.isStatic(method.getModifiers()) && method.getName().equals("box-impl")); + private final HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); @@ -321,7 +329,15 @@ public class InvocableHandlerMethod extends HandlerMethod { case INSTANCE -> argMap.put(parameter, target); case VALUE -> { if (!parameter.isOptional() || args[index] != null) { - argMap.put(parameter, args[index]); + if (parameter.getType().getClassifier() instanceof KClass kClass && kClass.isValue()) { + Class javaClass = JvmClassMappingKt.getJavaClass(kClass); + Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter); + Assert.state(methods.length == 1, "Unable to find a single box-impl synthetic static method in " + javaClass.getName()); + argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index])); + } + else { + argMap.put(parameter, args[index]); + } } index++; } 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 7226dea583..f253ea7df2 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 @@ -178,6 +178,22 @@ class InvocableHandlerMethodKotlinTests { assertHandlerResultValue(result, null) } + @Test + fun valueClass() { + this.resolvers.add(stubResolver(1L)) + val method = ValueClassController::valueClass.javaMethod!! + val result = invoke(ValueClassController(), method,1L) + assertHandlerResultValue(result, "1") + } + + @Test + fun valueClassDefaultValue() { + this.resolvers.add(stubResolver(Mono.empty())) + val method = ValueClassController::valueClassWithDefault.javaMethod!! + val result = invoke(ValueClassController(), method) + assertHandlerResultValue(result, "3.1") + } + private fun invokeForResult(handler: Any, method: Method, vararg providedArgs: Any): HandlerResult? { return invoke(handler, method, *providedArgs).block(Duration.ofSeconds(5)) @@ -266,4 +282,20 @@ class InvocableHandlerMethodKotlinTests { return null } } + + class ValueClassController { + + fun valueClass(limit: LongValueClass) = + "${limit.value}" + + fun valueClassWithDefault(limit: DoubleValueClass = DoubleValueClass(3.1)) = + "${limit.value}" + + } + + @JvmInline + value class LongValueClass(val value: Long) + + @JvmInline + value class DoubleValueClass(val value: Double) } \ No newline at end of file