From 5ae35f606c6779ab42f02967ee26c92d9f87620d Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 6 Sep 2017 14:39:56 +0200 Subject: [PATCH] Leverage kotlin-reflect to determine parameter names This is especially useful to determine interface parameter names without requiring Java 8 -parameters compiler flag. Issue: SPR-15541 --- .../core/DefaultParameterNameDiscoverer.java | 15 ++- ...tlinReflectionParameterNameDiscoverer.java | 105 ++++++++++++++++++ ...nReflectionParameterNameDiscovererTests.kt | 52 +++++++++ src/docs/asciidoc/kotlin.adoc | 7 +- 4 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java create mode 100644 spring-core/src/test/kotlin/org/springframework/core/KotlinReflectionParameterNameDiscovererTests.kt diff --git a/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java index fcd640714d..80f9dcfb2f 100644 --- a/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java +++ b/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 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. @@ -16,22 +16,35 @@ package org.springframework.core; +import org.springframework.util.ClassUtils; + /** * Default implementation of the {@link ParameterNameDiscoverer} strategy interface, * using the Java 8 standard reflection mechanism (if available), and falling back * to the ASM-based {@link LocalVariableTableParameterNameDiscoverer} for checking * debug information in the class file. * + *

If Kotlin is present, {@link KotlinReflectionParameterNameDiscoverer} is added first + * in the list and used for Kotlin classes and interfaces. + * *

Further discoverers may be added through {@link #addDiscoverer(ParameterNameDiscoverer)}. * * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 4.0 * @see StandardReflectionParameterNameDiscoverer * @see LocalVariableTableParameterNameDiscoverer + * @see KotlinReflectionParameterNameDiscoverer */ public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer { + private static final boolean kotlinPresent = + ClassUtils.isPresent("kotlin.Unit", DefaultParameterNameDiscoverer.class.getClassLoader()); + public DefaultParameterNameDiscoverer() { + if (kotlinPresent) { + addDiscoverer(new KotlinReflectionParameterNameDiscoverer()); + } addDiscoverer(new StandardReflectionParameterNameDiscoverer()); addDiscoverer(new LocalVariableTableParameterNameDiscoverer()); } diff --git a/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java new file mode 100644 index 0000000000..19f4468ac2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; +import java.util.stream.Collectors; + +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.jvm.ReflectJvmMapping; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * {@link ParameterNameDiscoverer} implementation which uses Kotlin's reflection facilities + * for introspecting parameter names. + * + * Compared to {@link StandardReflectionParameterNameDiscoverer}, it allows in addition to + * determine interface parameter names without requiring Java 8 -parameters compiler flag. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +public class KotlinReflectionParameterNameDiscoverer implements ParameterNameDiscoverer { + + @Nullable + private static final Class kotlinMetadata; + + static { + Class metadata; + try { + metadata = ClassUtils.forName("kotlin.Metadata", KotlinReflectionParameterNameDiscoverer.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // Kotlin API not available - no special support for Kotlin class instantiation + metadata = null; + } + kotlinMetadata = metadata; + } + + @Override + @Nullable + public String[] getParameterNames(Method method) { + if (!useKotlinSupport(method.getDeclaringClass())) { + return null; + } + KFunction function = ReflectJvmMapping.getKotlinFunction(method); + return (function != null ? getParameterNames(function.getParameters()) : null); + } + + @Override + @Nullable + public String[] getParameterNames(Constructor ctor) { + if (!useKotlinSupport(ctor.getDeclaringClass())) { + return null; + } + KFunction function = ReflectJvmMapping.getKotlinFunction(ctor); + return (function != null ? getParameterNames(function.getParameters()) : null); + } + + @Nullable + private String[] getParameterNames(List parameters) { + List filteredParameters = parameters + .stream() + .filter(p -> KParameter.Kind.VALUE.equals(p.getKind())) + .collect(Collectors.toList()); + String[] parameterNames = new String[filteredParameters.size()]; + for (int i = 0; i < filteredParameters.size(); i++) { + String name = filteredParameters.get(i).getName(); + if (name == null) { + return null; + } + parameterNames[i] = name; + } + return parameterNames; + } + + /** + * Return true if Kotlin is present and if the specified class is a Kotlin one. + */ + @SuppressWarnings("unchecked") + private static boolean useKotlinSupport(Class clazz) { + return (kotlinMetadata != null && + clazz.getDeclaredAnnotation((Class) kotlinMetadata) != null); + } + +} diff --git a/spring-core/src/test/kotlin/org/springframework/core/KotlinReflectionParameterNameDiscovererTests.kt b/spring-core/src/test/kotlin/org/springframework/core/KotlinReflectionParameterNameDiscovererTests.kt new file mode 100644 index 0000000000..5baa8b3091 --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/KotlinReflectionParameterNameDiscovererTests.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.core + +import org.junit.Test + +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.MatcherAssert.assertThat +import org.springframework.util.ReflectionUtils + +/** + * Tests for KotlinReflectionParameterNameDiscoverer + */ +class KotlinReflectionParameterNameDiscovererTests { + + private val parameterNameDiscoverer = KotlinReflectionParameterNameDiscoverer() + + @Test + fun getParameterNamesOnInterface() { + val method = ReflectionUtils.findMethod(MessageService::class.java,"sendMessage", String::class.java)!! + val actualParams = parameterNameDiscoverer.getParameterNames(method) + assertThat(actualParams, `is`(arrayOf("message"))) + } + + @Test + fun getParameterNamesOnClass() { + val method = ReflectionUtils.findMethod(MessageServiceImpl::class.java,"sendMessage", String::class.java)!! + val actualParams = parameterNameDiscoverer.getParameterNames(method) + assertThat(actualParams, `is`(arrayOf("message"))) + } + + interface MessageService { + fun sendMessage(message: String) + } + + class MessageServiceImpl { + fun sendMessage(message: String) = message + } +} diff --git a/src/docs/asciidoc/kotlin.adoc b/src/docs/asciidoc/kotlin.adoc index 40c5d2d331..67c33b7b57 100644 --- a/src/docs/asciidoc/kotlin.adoc +++ b/src/docs/asciidoc/kotlin.adoc @@ -115,12 +115,16 @@ Other libraries like Reactor or Spring Data leverage these annotations to provid null-safe APIs for Kotlin developers. ==== -== Classes +== Classes & Interfaces Spring Framework 5 now supports various Kotlin constructs like instantiating Kotlin classes via primary constructors, immutable classes data binding and function optional parameters with default values. +Kotlin parameter names are recognized via a dedicated `KotlinReflectionParameterNameDiscoverer` +which allows to find interface method parameter names without requiring Java 8 `-parameters` +compiler flag. + https://github.com/FasterXML/jackson-module-kotlin[Jackson Kotlin module] which is required for serializing / deserializing JSON data is automatically registered when present in the classpath, and will log a warning message if Jackson + Kotlin are detected without Jackson @@ -509,7 +513,6 @@ Here is a list of pending issues related to Spring + Kotlin support. ===== Spring Framework -* https://jira.spring.io/browse/SPR-15541[Leveraging kotlin-reflect to determine interface method parameters] * https://jira.spring.io/browse/SPR-15413[Add support for Kotlin coroutines] ===== Spring Boot