Skip to content

Commit a143b57

Browse files
committed
Polish Kotlin nullable support
This commit polishes Kotlin nullable support by reusing MethodParameter#isOptional() instead of adding a new MethodParameter#isNullable() method, adds Kotlin tests and introduces Spring Web Reactive support. Issue: SPR-14165
1 parent fada91e commit a143b57

File tree

10 files changed

+609
-63
lines changed

10 files changed

+609
-63
lines changed

build.gradle

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ buildscript {
55
dependencies {
66
classpath("org.springframework.build.gradle:propdeps-plugin:0.0.7")
77
classpath("org.asciidoctor:asciidoctor-gradle-plugin:1.5.3")
8-
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.4"
8+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.5-2"
99
classpath("io.spring.gradle:docbook-reference-plugin:0.3.1")
1010
}
1111
}
@@ -71,7 +71,7 @@ configure(allprojects) { project ->
7171
ext.junitPlatformVersion = '1.0.0-M2'
7272
ext.log4jVersion = '2.7'
7373
ext.nettyVersion = "4.1.6.Final"
74-
ext.kotlinVersion = "1.0.4"
74+
ext.kotlinVersion = "1.0.5-2"
7575
ext.okhttpVersion = "2.7.5"
7676
ext.okhttp3Version = "3.4.2"
7777
ext.poiVersion = "3.15"
@@ -569,6 +569,8 @@ project("spring-oxm") {
569569
project("spring-messaging") {
570570
description = "Spring Messaging"
571571

572+
apply plugin: "kotlin"
573+
572574
dependencies {
573575
compile(project(":spring-beans"))
574576
compile(project(":spring-core"))
@@ -604,6 +606,8 @@ project("spring-messaging") {
604606
testCompile("io.netty:netty-all:${nettyVersion}")
605607
testCompile("org.xmlunit:xmlunit-matchers:${xmlunitVersion}")
606608
testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}")
609+
testCompile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
610+
testCompile("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
607611
testRuntime("javax.activation:activation:${activationApiVersion}")
608612
testRuntime("com.sun.xml.bind:jaxb-core:${jaxbVersion}")
609613
testRuntime("com.sun.xml.bind:jaxb-impl:${jaxbVersion}")
@@ -710,7 +714,9 @@ project("spring-context-indexer") {
710714

711715
project("spring-web") {
712716
description = "Spring Web"
717+
713718
apply plugin: "groovy"
719+
apply plugin: "kotlin"
714720

715721
dependencies {
716722
compile(project(":spring-aop")) // for JaxWsPortProxyFactoryBean
@@ -781,6 +787,8 @@ project("spring-web") {
781787
testCompile("com.squareup.okhttp3:mockwebserver:${okhttp3Version}")
782788
testCompile("org.xmlunit:xmlunit-matchers:${xmlunitVersion}")
783789
testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}")
790+
testCompile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
791+
testCompile("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
784792
testRuntime("com.sun.mail:javax.mail:${javamailVersion}")
785793
testRuntime("com.sun.xml.bind:jaxb-core:${jaxbVersion}")
786794
testRuntime("com.sun.xml.bind:jaxb-impl:${jaxbVersion}")
@@ -797,6 +805,8 @@ project("spring-web") {
797805
project("spring-web-reactive") {
798806
description = "Spring Web Reactive"
799807

808+
apply plugin: "kotlin"
809+
800810
dependencies {
801811
compile(project(":spring-core"))
802812
compile(project(":spring-web"))
@@ -828,6 +838,8 @@ project("spring-web-reactive") {
828838
testCompile("com.fasterxml:aalto-xml:1.0.0")
829839
testCompile("org.xmlunit:xmlunit-matchers:${xmlunitVersion}")
830840
testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}")
841+
testCompile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
842+
testCompile("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
831843
testRuntime("javax.el:javax.el-api:${elApiVersion}")
832844
testRuntime("org.glassfish:javax.el:3.0.1-b08")
833845
testRuntime("com.sun.xml.bind:jaxb-core:${jaxbVersion}")

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

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -310,12 +310,12 @@ public MethodParameter nested() {
310310
}
311311

312312
/**
313-
* Return whether this method parameter is declared as optional
314-
* in the form of Java 8's {@link java.util.Optional}.
313+
* Return whether this method indicates a parameter which is not required
314+
* (either in the form of Java 8's {@link java.util.Optional} or Kotlin nullable type).
315315
* @since 4.3
316316
*/
317317
public boolean isOptional() {
318-
return (getParameterType() == Optional.class);
318+
return (getParameterType() == Optional.class || KotlinUtils.isNullable(this));
319319
}
320320

321321
/**
@@ -327,18 +327,7 @@ public boolean isOptional() {
327327
* @see #nested()
328328
*/
329329
public MethodParameter nestedIfOptional() {
330-
return (isOptional() ? nested() : this);
331-
}
332-
333-
/**
334-
* Return whether this method parameter is declared as a "nullable" value, if supported by
335-
* the underlying language. Currently the only supported language is Kotlin.
336-
* @since 5.0
337-
*/
338-
public boolean isNullable() {
339-
return KotlinUtils.isKotlinPresent() &&
340-
KotlinUtils.isKotlinClass(getContainingClass()) &&
341-
KotlinUtils.isNullable(this.parameterIndex, this.method, this.constructor);
330+
return (getParameterType() == Optional.class ? nested() : this);
342331
}
343332

344333
/**

spring-core/src/main/java/org/springframework/util/KotlinUtils.java

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import kotlin.reflect.jvm.ReflectJvmMapping;
2323
import org.springframework.core.MethodParameter;
2424

25-
import java.lang.reflect.Constructor;
2625
import java.lang.reflect.Method;
2726
import java.util.List;
2827
import java.util.stream.Collectors;
@@ -31,44 +30,55 @@
3130
* Miscellaneous Kotlin utility methods.
3231
*
3332
* @author Raman Gupta
33+
* @author Sebastien Deleuze
3434
* @since 5.0
3535
*/
36-
public class KotlinUtils {
36+
public abstract class KotlinUtils {
3737

38-
private static final boolean kotlinPresent;
39-
40-
static {
41-
kotlinPresent = ClassUtils.isPresent("kotlin.Unit", MethodParameter.class.getClassLoader());
42-
}
38+
private static final boolean kotlinPresent = ClassUtils.isPresent("kotlin.Unit", KotlinUtils.class.getClassLoader());
4339

40+
/**
41+
* Return whether Kotlin is available on the classpath or not.
42+
*/
4443
public static boolean isKotlinPresent() {
4544
return kotlinPresent;
4645
}
4746

47+
/**
48+
* Return whether the specified type is a Kotlin class or not.
49+
*/
4850
public static boolean isKotlinClass(Class<?> type) {
49-
return type != null && type.getDeclaredAnnotation(Metadata.class) != null;
51+
Assert.notNull(type, "Type must not be null");
52+
return isKotlinPresent() && type.getDeclaredAnnotation(Metadata.class) != null;
5053
}
5154

52-
public static boolean isNullable(int parameterIndex, Method method, Constructor<?> constructor) {
53-
if(parameterIndex < 0) {
54-
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
55-
return function != null && function.getReturnType().isMarkedNullable();
56-
} else {
57-
KFunction<?> function = method != null ?
58-
ReflectJvmMapping.getKotlinFunction(method) :
59-
ReflectJvmMapping.getKotlinFunction(constructor);
60-
if(function != null) {
61-
@SuppressWarnings("unchecked")
62-
List<KParameter> parameters = function.getParameters();
63-
return parameters
64-
.stream()
65-
.filter(p -> KParameter.Kind.VALUE.equals(p.getKind()))
66-
.collect(Collectors.toList())
67-
.get(parameterIndex)
68-
.getType()
69-
.isMarkedNullable();
55+
/**
56+
* Check whether the specified {@link MethodParameter} represents a nullable Kotlin type or not.
57+
*/
58+
public static boolean isNullable(MethodParameter methodParameter) {
59+
Method method = methodParameter.getMethod();
60+
int parameterIndex = methodParameter.getParameterIndex();
61+
if (isKotlinClass(methodParameter.getContainingClass())) {
62+
if (parameterIndex < 0) {
63+
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
64+
return function != null && function.getReturnType().isMarkedNullable();
65+
}
66+
else {
67+
KFunction<?> function = (method != null ? ReflectJvmMapping.getKotlinFunction(method) :
68+
ReflectJvmMapping.getKotlinFunction(methodParameter.getConstructor()));
69+
if (function != null) {
70+
List<KParameter> parameters = function.getParameters();
71+
return parameters
72+
.stream()
73+
.filter(p -> KParameter.Kind.VALUE.equals(p.getKind()))
74+
.collect(Collectors.toList())
75+
.get(parameterIndex)
76+
.getType()
77+
.isMarkedNullable();
78+
}
7079
}
71-
return false;
7280
}
81+
return false;
7382
}
83+
7484
}

spring-core/src/test/kotlin/org/springframework/util/KotlinUtilsTests.kt

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,22 @@ import org.springframework.core.MethodParameter
1111
import org.springframework.util.KotlinUtils.*
1212

1313
/**
14-
* Unit tests for [KotlinUtils].
15-
14+
* Test fixture for [KotlinUtils].
15+
*
1616
* @author Raman Gupta
17+
* @author Sebastien Deleuze
1718
*/
1819
class KotlinUtilsTests {
1920

20-
private lateinit var methodNullable: Method
21-
private lateinit var methodNonNullable: Method
21+
lateinit var nullableMethod: Method
22+
23+
lateinit var nonNullableMethod: Method
2224

2325
@Before
2426
@Throws(NoSuchMethodException::class)
25-
fun setUp() {
26-
methodNullable = javaClass.getMethod("methodNullable", String::class.java, java.lang.Long.TYPE)
27-
methodNonNullable = javaClass.getMethod("methodNonNullable", String::class.java, java.lang.Long.TYPE)
27+
fun setup() {
28+
nullableMethod = javaClass.getMethod("nullable", String::class.java)
29+
nonNullableMethod = javaClass.getMethod("nonNullable", String::class.java)
2830
}
2931

3032
@Test
@@ -35,26 +37,26 @@ class KotlinUtilsTests {
3537

3638
@Test
3739
fun `Are kotlin classes detected`() {
38-
assertFalse(isKotlinClass(null))
3940
assertFalse(isKotlinClass(MethodParameter::class.java))
4041
assertTrue(isKotlinClass(javaClass))
4142
}
4243

4344
@Test
4445
fun `Obtains method return type nullability`() {
45-
assertTrue(isNullable(-1, methodNullable, null))
46-
assertFalse(isNullable(-1, methodNonNullable, null))
46+
assertTrue(isNullable(MethodParameter(nullableMethod, -1)))
47+
assertFalse(isNullable(MethodParameter(nonNullableMethod, -1)))
4748
}
4849

4950
@Test
5051
fun `Obtains method parameter nullability`() {
51-
assertTrue(isNullable(0, methodNullable, null))
52-
assertFalse(isNullable(1, methodNullable, null))
52+
assertTrue(isNullable(MethodParameter(nullableMethod, 0)))
53+
assertFalse(isNullable(MethodParameter(nonNullableMethod, 0)))
5354
}
5455

5556
@Suppress("unused", "unused_parameter")
56-
fun methodNullable(p1: String?, p2: Long): Int? = 42
57+
fun nullable(p1: String?): Int? = 42
5758

5859
@Suppress("unused", "unused_parameter")
59-
fun methodNonNullable(p1: String?, p2: Long): Int = 42
60+
fun nonNullable(p1: String): Int = 42
61+
6062
}

spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public Object resolveArgument(MethodParameter parameter, Message<?> message) thr
9898
if (namedValueInfo.defaultValue != null) {
9999
arg = resolveStringValue(namedValueInfo.defaultValue);
100100
}
101-
else if (namedValueInfo.required && !nestedParameter.isOptional() && !nestedParameter.isNullable()) {
101+
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
102102
handleMissingValue(namedValueInfo.name, nestedParameter, message);
103103
}
104104
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());

0 commit comments

Comments
 (0)