Support Kotlin parameter default values in handler methods

This commit adds support for Kotlin parameter default values
in handler methods. It allows to write:
@RequestParam value: String = "default"
as an alternative to:
@RequestParam(defaultValue = "default") value: String

Both Spring MVC and WebFlux are supported, including on
suspending functions.

Closes gh-21139
This commit is contained in:
Sébastien Deleuze
2023-06-21 18:49:11 +02:00
parent 254fb39567
commit f06cf21341
12 changed files with 679 additions and 41 deletions

View File

@@ -18,6 +18,7 @@ package org.springframework.core;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import kotlin.Unit;
@@ -26,6 +27,7 @@ import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KClass;
import kotlin.reflect.KClassifier;
import kotlin.reflect.KFunction;
import kotlin.reflect.KParameter;
import kotlin.reflect.full.KCallables;
import kotlin.reflect.jvm.KCallablesJvm;
import kotlin.reflect.jvm.ReflectJvmMapping;
@@ -42,6 +44,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Utilities for working with Kotlin Coroutines.
@@ -104,8 +107,22 @@ public abstract class CoroutinesUtils {
if (method.isAccessible() && !KCallablesJvm.isAccessible(function)) {
KCallablesJvm.setAccessible(function, true);
}
Mono<Object> mono = MonoKt.mono(context, (scope, continuation) ->
KCallables.callSuspend(function, getSuspendedFunctionArgs(method, target, args), continuation))
Mono<Object> mono = MonoKt.mono(context, (scope, continuation) -> {
Map<KParameter, Object> argMap = CollectionUtils.newHashMap(args.length + 1);
int index = 0;
for (KParameter parameter : function.getParameters()) {
switch (parameter.getKind()) {
case INSTANCE -> argMap.put(parameter, target);
case VALUE -> {
if (!parameter.isOptional() || args[index] != null) {
argMap.put(parameter, args[index]);
}
index++;
}
}
}
return KCallables.callSuspendBy(function, argMap, continuation);
})
.filter(result -> !Objects.equals(result, Unit.INSTANCE))
.onErrorMap(InvocationTargetException.class, InvocationTargetException::getTargetException);
@@ -125,14 +142,6 @@ public abstract class CoroutinesUtils {
return mono;
}
private static Object[] getSuspendedFunctionArgs(Method method, Object target, Object... args) {
int length = (args.length == method.getParameterCount() - 1 ? args.length + 1 : args.length);
Object[] functionArgs = new Object[length];
functionArgs[0] = target;
System.arraycopy(args, 0, functionArgs, 1, length - 1);
return functionArgs;
}
private static Flux<?> asFlux(Object flow) {
return ReactorFlowKt.asFlux(((Flow<?>) flow));
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -38,6 +38,8 @@ class KotlinMethodParameterTests {
private val nonNullableMethod = javaClass.getMethod("nonNullable", String::class.java)
private val withDefaultValueMethod: Method = javaClass.getMethod("withDefaultValue", String::class.java)
private val innerClassConstructor = InnerClass::class.java.getConstructor(KotlinMethodParameterTests::class.java)
private val innerClassWithParametersConstructor = InnerClassWithParameter::class.java
@@ -52,6 +54,16 @@ class KotlinMethodParameterTests {
assertThat(MethodParameter(nonNullableMethod, 0).isOptional).isFalse()
}
@Test
fun `Method parameter with default value`() {
assertThat(MethodParameter(withDefaultValueMethod, 0).isOptional).isTrue()
}
@Test
fun `Method parameter without default value`() {
assertThat(MethodParameter(nonNullableMethod, 0).isOptional).isFalse()
}
@Test
fun `Method return type nullability`() {
assertThat(MethodParameter(nullableMethod, -1).isOptional).isTrue()
@@ -123,6 +135,8 @@ class KotlinMethodParameterTests {
@Suppress("unused_parameter")
fun nonNullable(nonNullable: String): Int = 42
fun withDefaultValue(withDefaultValue: String = "default") = withDefaultValue
inner class InnerClass
@Suppress("unused_parameter")