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

@@ -16,16 +16,22 @@
package org.springframework.web.method.annotation;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import jakarta.servlet.ServletException;
import kotlin.reflect.KFunction;
import kotlin.reflect.KParameter;
import kotlin.reflect.jvm.ReflectJvmMapping;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.ServletRequestBindingException;
@@ -60,6 +66,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 3.1
*/
public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver {
@@ -98,6 +105,9 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
boolean hasDefaultValue = KotlinDetector.isKotlinReflectPresent()
&& KotlinDetector.isKotlinType(parameter.getDeclaringClass())
&& KotlinDelegate.hasDefaultValue(nestedParameter);
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
@@ -113,13 +123,15 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
if (!hasDefaultValue) {
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
if (binderFactory != null) {
if (binderFactory != null && (arg != null || !hasDefaultValue)) {
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
try {
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
@@ -304,4 +316,27 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
}
}
/**
* Inner class to avoid a hard dependency on Kotlin at runtime.
*/
private static class KotlinDelegate {
/**
* Check whether the specified {@link MethodParameter} represents a nullable Kotlin type
* or an optional parameter (with a default value in the Kotlin declaration).
*/
public static boolean hasDefaultValue(MethodParameter parameter) {
Method method = Objects.requireNonNull(parameter.getMethod());
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
if (function != null) {
int index = 0;
for (KParameter kParameter : function.getParameters()) {
if (KParameter.Kind.VALUE.equals(kParameter.getKind()) && parameter.getParameterIndex() == index++) {
return kParameter.isOptional();
}
}
}
return false;
}
}
}

View File

@@ -19,7 +19,13 @@ package org.springframework.web.method.support;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import kotlin.reflect.KFunction;
import kotlin.reflect.KParameter;
import kotlin.reflect.jvm.KCallablesJvm;
import kotlin.reflect.jvm.ReflectJvmMapping;
import org.reactivestreams.Publisher;
import org.springframework.context.MessageSource;
@@ -29,6 +35,7 @@ import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.beanvalidation.MethodValidator;
import org.springframework.web.bind.WebDataBinder;
@@ -236,8 +243,13 @@ public class InvocableHandlerMethod extends HandlerMethod {
protected Object doInvoke(Object... args) throws Exception {
Method method = getBridgedMethod();
try {
if (KotlinDetector.isSuspendingFunction(method)) {
return invokeSuspendingFunction(method, getBean(), args);
if (KotlinDetector.isKotlinReflectPresent()) {
if (KotlinDetector.isSuspendingFunction(method)) {
return invokeSuspendingFunction(method, getBean(), args);
}
else if (KotlinDetector.isKotlinType(method.getDeclaringClass())) {
return KotlinDelegate.invokeFunction(method, getBean(), args);
}
}
return method.invoke(getBean(), args);
}
@@ -279,4 +291,33 @@ public class InvocableHandlerMethod extends HandlerMethod {
return CoroutinesUtils.invokeSuspendingFunction(method, target, args);
}
/**
* Inner class to avoid a hard dependency on Kotlin at runtime.
*/
private static class KotlinDelegate {
@Nullable
@SuppressWarnings("deprecation")
public static Object invokeFunction(Method method, Object target, Object[] args) {
KFunction<?> function = Objects.requireNonNull(ReflectJvmMapping.getKotlinFunction(method));
if (method.isAccessible() && !KCallablesJvm.isAccessible(function)) {
KCallablesJvm.setAccessible(function, true);
}
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 function.callBy(argMap);
}
}
}