Skip to content

Commit 5ae35f6

Browse files
committed
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
1 parent 02a2c40 commit 5ae35f6

File tree

4 files changed

+176
-3
lines changed

4 files changed

+176
-3
lines changed

spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,22 +16,35 @@
1616

1717
package org.springframework.core;
1818

19+
import org.springframework.util.ClassUtils;
20+
1921
/**
2022
* Default implementation of the {@link ParameterNameDiscoverer} strategy interface,
2123
* using the Java 8 standard reflection mechanism (if available), and falling back
2224
* to the ASM-based {@link LocalVariableTableParameterNameDiscoverer} for checking
2325
* debug information in the class file.
2426
*
27+
* <p>If Kotlin is present, {@link KotlinReflectionParameterNameDiscoverer} is added first
28+
* in the list and used for Kotlin classes and interfaces.
29+
*
2530
* <p>Further discoverers may be added through {@link #addDiscoverer(ParameterNameDiscoverer)}.
2631
*
2732
* @author Juergen Hoeller
33+
* @author Sebastien Deleuze
2834
* @since 4.0
2935
* @see StandardReflectionParameterNameDiscoverer
3036
* @see LocalVariableTableParameterNameDiscoverer
37+
* @see KotlinReflectionParameterNameDiscoverer
3138
*/
3239
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {
3340

41+
private static final boolean kotlinPresent =
42+
ClassUtils.isPresent("kotlin.Unit", DefaultParameterNameDiscoverer.class.getClassLoader());
43+
3444
public DefaultParameterNameDiscoverer() {
45+
if (kotlinPresent) {
46+
addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
47+
}
3548
addDiscoverer(new StandardReflectionParameterNameDiscoverer());
3649
addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
3750
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.core;
18+
19+
import java.lang.annotation.Annotation;
20+
import java.lang.reflect.Constructor;
21+
import java.lang.reflect.Method;
22+
import java.util.List;
23+
import java.util.stream.Collectors;
24+
25+
import kotlin.reflect.KFunction;
26+
import kotlin.reflect.KParameter;
27+
import kotlin.reflect.jvm.ReflectJvmMapping;
28+
29+
import org.springframework.lang.Nullable;
30+
import org.springframework.util.ClassUtils;
31+
32+
/**
33+
* {@link ParameterNameDiscoverer} implementation which uses Kotlin's reflection facilities
34+
* for introspecting parameter names.
35+
*
36+
* Compared to {@link StandardReflectionParameterNameDiscoverer}, it allows in addition to
37+
* determine interface parameter names without requiring Java 8 -parameters compiler flag.
38+
*
39+
* @author Sebastien Deleuze
40+
* @since 5.0
41+
*/
42+
public class KotlinReflectionParameterNameDiscoverer implements ParameterNameDiscoverer {
43+
44+
@Nullable
45+
private static final Class<?> kotlinMetadata;
46+
47+
static {
48+
Class<?> metadata;
49+
try {
50+
metadata = ClassUtils.forName("kotlin.Metadata", KotlinReflectionParameterNameDiscoverer.class.getClassLoader());
51+
}
52+
catch (ClassNotFoundException ex) {
53+
// Kotlin API not available - no special support for Kotlin class instantiation
54+
metadata = null;
55+
}
56+
kotlinMetadata = metadata;
57+
}
58+
59+
@Override
60+
@Nullable
61+
public String[] getParameterNames(Method method) {
62+
if (!useKotlinSupport(method.getDeclaringClass())) {
63+
return null;
64+
}
65+
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
66+
return (function != null ? getParameterNames(function.getParameters()) : null);
67+
}
68+
69+
@Override
70+
@Nullable
71+
public String[] getParameterNames(Constructor<?> ctor) {
72+
if (!useKotlinSupport(ctor.getDeclaringClass())) {
73+
return null;
74+
}
75+
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(ctor);
76+
return (function != null ? getParameterNames(function.getParameters()) : null);
77+
}
78+
79+
@Nullable
80+
private String[] getParameterNames(List<KParameter> parameters) {
81+
List<KParameter> filteredParameters = parameters
82+
.stream()
83+
.filter(p -> KParameter.Kind.VALUE.equals(p.getKind()))
84+
.collect(Collectors.toList());
85+
String[] parameterNames = new String[filteredParameters.size()];
86+
for (int i = 0; i < filteredParameters.size(); i++) {
87+
String name = filteredParameters.get(i).getName();
88+
if (name == null) {
89+
return null;
90+
}
91+
parameterNames[i] = name;
92+
}
93+
return parameterNames;
94+
}
95+
96+
/**
97+
* Return true if Kotlin is present and if the specified class is a Kotlin one.
98+
*/
99+
@SuppressWarnings("unchecked")
100+
private static boolean useKotlinSupport(Class<?> clazz) {
101+
return (kotlinMetadata != null &&
102+
clazz.getDeclaredAnnotation((Class<? extends Annotation>) kotlinMetadata) != null);
103+
}
104+
105+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.core
17+
18+
import org.junit.Test
19+
20+
import org.hamcrest.CoreMatchers.`is`
21+
import org.hamcrest.MatcherAssert.assertThat
22+
import org.springframework.util.ReflectionUtils
23+
24+
/**
25+
* Tests for KotlinReflectionParameterNameDiscoverer
26+
*/
27+
class KotlinReflectionParameterNameDiscovererTests {
28+
29+
private val parameterNameDiscoverer = KotlinReflectionParameterNameDiscoverer()
30+
31+
@Test
32+
fun getParameterNamesOnInterface() {
33+
val method = ReflectionUtils.findMethod(MessageService::class.java,"sendMessage", String::class.java)!!
34+
val actualParams = parameterNameDiscoverer.getParameterNames(method)
35+
assertThat(actualParams, `is`(arrayOf("message")))
36+
}
37+
38+
@Test
39+
fun getParameterNamesOnClass() {
40+
val method = ReflectionUtils.findMethod(MessageServiceImpl::class.java,"sendMessage", String::class.java)!!
41+
val actualParams = parameterNameDiscoverer.getParameterNames(method)
42+
assertThat(actualParams, `is`(arrayOf("message")))
43+
}
44+
45+
interface MessageService {
46+
fun sendMessage(message: String)
47+
}
48+
49+
class MessageServiceImpl {
50+
fun sendMessage(message: String) = message
51+
}
52+
}

src/docs/asciidoc/kotlin.adoc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,16 @@ Other libraries like Reactor or Spring Data leverage these annotations to provid
115115
null-safe APIs for Kotlin developers.
116116
====
117117

118-
== Classes
118+
== Classes & Interfaces
119119

120120
Spring Framework 5 now supports various Kotlin constructs like instantiating Kotlin classes
121121
via primary constructors, immutable classes data binding and function optional parameters
122122
with default values.
123123

124+
Kotlin parameter names are recognized via a dedicated `KotlinReflectionParameterNameDiscoverer`
125+
which allows to find interface method parameter names without requiring Java 8 `-parameters`
126+
compiler flag.
127+
124128
https://github.com/FasterXML/jackson-module-kotlin[Jackson Kotlin module] which is required
125129
for serializing / deserializing JSON data is automatically registered when present in the
126130
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.
509513

510514
===== Spring Framework
511515

512-
* https://jira.spring.io/browse/SPR-15541[Leveraging kotlin-reflect to determine interface method parameters]
513516
* https://jira.spring.io/browse/SPR-15413[Add support for Kotlin coroutines]
514517

515518
===== Spring Boot

0 commit comments

Comments
 (0)