Add support for @RequestMapping on Kotlin property accessors

This commit refines InvocableHandlerMethod (both Servlet and
Reactive variants) in order to support annotated property
accessors as they translate into regular Java methods, instead
of throwing a NullPointerException.

Closes gh-31856
This commit is contained in:
Sébastien Deleuze 2023-12-19 12:24:13 +01:00
parent 917978cbc2
commit 12f01f9b5f
4 changed files with 40 additions and 6 deletions

View File

@ -21,7 +21,6 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import kotlin.Unit; import kotlin.Unit;
import kotlin.jvm.JvmClassMappingKt; import kotlin.jvm.JvmClassMappingKt;
@ -306,8 +305,12 @@ public class InvocableHandlerMethod extends HandlerMethod {
@Nullable @Nullable
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public static Object invokeFunction(Method method, Object target, Object[] args) { public static Object invokeFunction(Method method, Object target, Object[] args) throws InvocationTargetException, IllegalAccessException {
KFunction<?> function = Objects.requireNonNull(ReflectJvmMapping.getKotlinFunction(method)); KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
// For property accessors
if (function == null) {
return method.invoke(target, args);
}
if (method.isAccessible() && !KCallablesJvm.isAccessible(function)) { if (method.isAccessible() && !KCallablesJvm.isAccessible(function)) {
KCallablesJvm.setAccessible(function, true); KCallablesJvm.setAccessible(function, true);
} }

View File

@ -98,6 +98,12 @@ class InvocableHandlerMethodKotlinTests {
Assertions.assertThat(value).isEqualTo(3.1) Assertions.assertThat(value).isEqualTo(3.1)
} }
@Test
fun propertyAccessor() {
val value = getInvocable(PropertyAccessorHandler::class.java).invokeForRequest(request, null)
Assertions.assertThat(value).isEqualTo("foo")
}
private fun getInvocable(clazz: Class<*>, vararg argTypes: Class<*>): InvocableHandlerMethod { private fun getInvocable(clazz: Class<*>, vararg argTypes: Class<*>): InvocableHandlerMethod {
val method = ResolvableMethod.on(clazz).argTypes(*argTypes).resolveMethod() val method = ResolvableMethod.on(clazz).argTypes(*argTypes).resolveMethod()
val handlerMethod = InvocableHandlerMethod(clazz.constructors.first().newInstance(), method) val handlerMethod = InvocableHandlerMethod(clazz.constructors.first().newInstance(), method)
@ -138,6 +144,12 @@ class InvocableHandlerMethodKotlinTests {
limit.value limit.value
} }
private class PropertyAccessorHandler {
val prop: String
get() = "foo"
}
@JvmInline @JvmInline
value class LongValueClass(val value: Long) value class LongValueClass(val value: Long)

View File

@ -24,7 +24,6 @@ import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream; import java.util.stream.Stream;
import kotlin.Unit; import kotlin.Unit;
@ -306,7 +305,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
@Nullable @Nullable
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public static Object invokeFunction(Method method, Object target, Object[] args, boolean isSuspendingFunction, public static Object invokeFunction(Method method, Object target, Object[] args, boolean isSuspendingFunction,
ServerWebExchange exchange) { ServerWebExchange exchange) throws InvocationTargetException, IllegalAccessException {
if (isSuspendingFunction) { if (isSuspendingFunction) {
Object coroutineContext = exchange.getAttribute(COROUTINE_CONTEXT_ATTRIBUTE); Object coroutineContext = exchange.getAttribute(COROUTINE_CONTEXT_ATTRIBUTE);
@ -318,7 +317,11 @@ public class InvocableHandlerMethod extends HandlerMethod {
} }
} }
else { else {
KFunction<?> function = Objects.requireNonNull(ReflectJvmMapping.getKotlinFunction(method)); KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
// For property accessors
if (function == null) {
return method.invoke(target, args);
}
if (method.isAccessible() && !KCallablesJvm.isAccessible(function)) { if (method.isAccessible() && !KCallablesJvm.isAccessible(function)) {
KCallablesJvm.setAccessible(function, true); KCallablesJvm.setAccessible(function, true);
} }

View File

@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test
import org.springframework.core.ReactiveAdapterRegistry import org.springframework.core.ReactiveAdapterRegistry
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.server.reactive.ServerHttpResponse import org.springframework.http.server.reactive.ServerHttpResponse
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.reactive.BindingContext import org.springframework.web.reactive.BindingContext
@ -194,6 +195,14 @@ class InvocableHandlerMethodKotlinTests {
assertHandlerResultValue(result, "3.1") assertHandlerResultValue(result, "3.1")
} }
@Test
fun propertyAccessor() {
this.resolvers.add(stubResolver(Mono.empty()))
val method = PropertyAccessorController::prop.getter.javaMethod!!
val result = invoke(PropertyAccessorController(), method)
assertHandlerResultValue(result, "foo")
}
private fun invokeForResult(handler: Any, method: Method, vararg providedArgs: Any): HandlerResult? { private fun invokeForResult(handler: Any, method: Method, vararg providedArgs: Any): HandlerResult? {
return invoke(handler, method, *providedArgs).block(Duration.ofSeconds(5)) return invoke(handler, method, *providedArgs).block(Duration.ofSeconds(5))
@ -293,6 +302,13 @@ class InvocableHandlerMethodKotlinTests {
} }
class PropertyAccessorController {
val prop: String
@GetMapping("/")
get() = "foo"
}
@JvmInline @JvmInline
value class LongValueClass(val value: Long) value class LongValueClass(val value: Long)