Skip to content

Commit 39d2769

Browse files
committed
Autodetect Kotlin nullability for optional injection points (analogous to java.util.Optional)
Built-in support in MethodParameter and DependencyDescriptor supersedes our separate KotlinUtils helper. Issue: SPR-14951
1 parent 361ab6b commit 39d2769

File tree

7 files changed

+257
-150
lines changed

7 files changed

+257
-150
lines changed

build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ configure(subprojects - project(":spring-build-src")) { subproject ->
312312

313313
project("spring-build-src") {
314314
description = "Exposes gradle buildSrc for IDE support"
315+
315316
apply plugin: "groovy"
316317

317318
dependencies {
@@ -427,18 +428,24 @@ project("spring-core") {
427428
project("spring-beans") {
428429
description = "Spring Beans"
429430

431+
// Disabled since Kotlin compiler does not support JDK 9 yet
432+
//apply plugin: "kotlin"
433+
430434
dependencies {
431435
compile(project(":spring-core"))
432436
compile(files(project(":spring-core").cglibRepackJar))
433437
optional("javax.inject:javax.inject:1")
434438
optional("javax.el:javax.el-api:${elApiVersion}")
439+
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
440+
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
435441
optional("org.yaml:snakeyaml:${snakeyamlVersion}")
436442
testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}")
437443
}
438444
}
439445

440446
project("spring-beans-groovy") {
441447
description "Groovy Bean Definitions"
448+
442449
merge.into = project(":spring-beans")
443450
apply plugin: "groovy"
444451

@@ -501,6 +508,7 @@ project("spring-instrument") {
501508

502509
project("spring-context") {
503510
description = "Spring Context"
511+
504512
apply plugin: "groovy"
505513

506514
dependencies {

spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
import java.lang.reflect.ParameterizedType;
2424
import java.lang.reflect.Type;
2525
import java.util.Map;
26+
import java.util.Optional;
27+
28+
import kotlin.Metadata;
29+
import kotlin.reflect.KProperty;
30+
import kotlin.reflect.jvm.ReflectJvmMapping;
2631

2732
import org.springframework.beans.BeansException;
2833
import org.springframework.beans.factory.BeanFactory;
@@ -33,6 +38,7 @@
3338
import org.springframework.core.MethodParameter;
3439
import org.springframework.core.ParameterNameDiscoverer;
3540
import org.springframework.core.ResolvableType;
41+
import org.springframework.util.ClassUtils;
3642

3743
/**
3844
* Descriptor for a specific dependency that is about to be injected.
@@ -45,6 +51,10 @@
4551
@SuppressWarnings("serial")
4652
public class DependencyDescriptor extends InjectionPoint implements Serializable {
4753

54+
private static final boolean kotlinPresent =
55+
ClassUtils.isPresent("kotlin.Unit", DependencyDescriptor.class.getClassLoader());
56+
57+
4858
private final Class<?> declaringClass;
4959

5060
private String methodName;
@@ -83,6 +93,7 @@ public DependencyDescriptor(MethodParameter methodParameter, boolean required) {
8393
*/
8494
public DependencyDescriptor(MethodParameter methodParameter, boolean required, boolean eager) {
8595
super(methodParameter);
96+
8697
this.declaringClass = methodParameter.getDeclaringClass();
8798
if (this.methodParameter.getMethod() != null) {
8899
this.methodName = methodParameter.getMethod().getName();
@@ -116,6 +127,7 @@ public DependencyDescriptor(Field field, boolean required) {
116127
*/
117128
public DependencyDescriptor(Field field, boolean required, boolean eager) {
118129
super(field);
130+
119131
this.declaringClass = field.getDeclaringClass();
120132
this.fieldName = field.getName();
121133
this.required = required;
@@ -128,6 +140,7 @@ public DependencyDescriptor(Field field, boolean required, boolean eager) {
128140
*/
129141
public DependencyDescriptor(DependencyDescriptor original) {
130142
super(original);
143+
131144
this.declaringClass = original.declaringClass;
132145
this.methodName = original.methodName;
133146
this.parameterTypes = original.parameterTypes;
@@ -144,7 +157,17 @@ public DependencyDescriptor(DependencyDescriptor original) {
144157
* Return whether this dependency is required.
145158
*/
146159
public boolean isRequired() {
147-
return this.required;
160+
if (!this.required) {
161+
return false;
162+
}
163+
164+
if (this.field != null) {
165+
return !(this.field.getType() == Optional.class ||
166+
(kotlinPresent && KotlinDelegate.isNullable(this.field)));
167+
}
168+
else {
169+
return !this.methodParameter.isOptional();
170+
}
148171
}
149172

150173
/**
@@ -398,4 +421,22 @@ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFound
398421
}
399422
}
400423

424+
425+
/**
426+
* Inner class to avoid a hard dependency on Kotlin at runtime.
427+
*/
428+
private static class KotlinDelegate {
429+
430+
/**
431+
* Check whether the specified {@link Field} represents a nullable Kotlin type or not.
432+
*/
433+
public static boolean isNullable(Field field) {
434+
if (field.getDeclaringClass().isAnnotationPresent(Metadata.class)) {
435+
KProperty<?> property = ReflectJvmMapping.getKotlinProperty(field);
436+
return (property != null && property.getReturnType().isMarkedNullable());
437+
}
438+
return false;
439+
}
440+
}
441+
401442
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2002-2016 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.beans.factory.annotation
18+
19+
import java.lang.reflect.Method
20+
21+
import org.junit.Before
22+
import org.junit.Test
23+
24+
import org.springframework.beans.factory.annotation.Autowired
25+
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
26+
import org.springframework.beans.factory.support.DefaultListableBeanFactory
27+
import org.springframework.beans.factory.support.RootBeanDefinition
28+
import org.springframework.tests.sample.beans.TestBean
29+
30+
import org.junit.Assert.*
31+
32+
/**
33+
* Tests for Kotlin support with [@Autowired].
34+
*
35+
* @author Juergen Hoeller
36+
*/
37+
class KotlinAutowiredTests {
38+
39+
@Test
40+
fun autowiringWithTarget() {
41+
var bf = DefaultListableBeanFactory()
42+
var bpp = AutowiredAnnotationBeanPostProcessor()
43+
bpp.setBeanFactory(bf)
44+
bf.addBeanPostProcessor(bpp)
45+
var bd = RootBeanDefinition(KotlinBean::class.java)
46+
bd.setScope(RootBeanDefinition.SCOPE_PROTOTYPE)
47+
bf.registerBeanDefinition("annotatedBean", bd)
48+
var tb = TestBean()
49+
bf.registerSingleton("testBean", tb)
50+
51+
var kb = bf.getBean("annotatedBean", KotlinBean::class.java)
52+
assertSame(tb, kb.injectedFromConstructor)
53+
assertSame(tb, kb.injectedFromMethod)
54+
assertSame(tb, kb.injectedField)
55+
}
56+
57+
@Test
58+
fun autowiringWithoutTarget() {
59+
var bf = DefaultListableBeanFactory()
60+
var bpp = AutowiredAnnotationBeanPostProcessor()
61+
bpp.setBeanFactory(bf)
62+
bf.addBeanPostProcessor(bpp)
63+
var bd = RootBeanDefinition(KotlinBean::class.java)
64+
bd.setScope(RootBeanDefinition.SCOPE_PROTOTYPE)
65+
bf.registerBeanDefinition("annotatedBean", bd)
66+
67+
var kb = bf.getBean("annotatedBean", KotlinBean::class.java)
68+
assertNull(kb.injectedFromConstructor)
69+
assertNull(kb.injectedFromMethod)
70+
assertNull(kb.injectedField)
71+
}
72+
73+
74+
class KotlinBean(val injectedFromConstructor: TestBean?) {
75+
76+
var injectedFromMethod: TestBean? = null
77+
78+
@Autowired
79+
var injectedField: TestBean? = null
80+
81+
@Autowired
82+
fun injectedMethod(p1: TestBean?) {
83+
injectedFromMethod = p1
84+
}
85+
}
86+
87+
}

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

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,18 @@
2626
import java.lang.reflect.ParameterizedType;
2727
import java.lang.reflect.Type;
2828
import java.util.HashMap;
29+
import java.util.List;
2930
import java.util.Map;
3031
import java.util.Optional;
32+
import java.util.stream.Collectors;
33+
34+
import kotlin.Metadata;
35+
import kotlin.reflect.KFunction;
36+
import kotlin.reflect.KParameter;
37+
import kotlin.reflect.jvm.ReflectJvmMapping;
3138

3239
import org.springframework.util.Assert;
33-
import org.springframework.util.KotlinUtils;
40+
import org.springframework.util.ClassUtils;
3441

3542
/**
3643
* Helper class that encapsulates the specification of a method parameter, i.e. a {@link Method}
@@ -45,12 +52,17 @@
4552
* @author Rob Harrop
4653
* @author Andy Clement
4754
* @author Sam Brannen
55+
* @author Sebastien Deleuze
4856
* @since 2.0
4957
* @see GenericCollectionTypeResolver
5058
* @see org.springframework.core.annotation.SynthesizingMethodParameter
5159
*/
5260
public class MethodParameter {
5361

62+
private static final boolean kotlinPresent =
63+
ClassUtils.isPresent("kotlin.Unit", MethodParameter.class.getClassLoader());
64+
65+
5466
private final Method method;
5567

5668
private final Constructor<?> constructor;
@@ -311,11 +323,13 @@ public MethodParameter nested() {
311323

312324
/**
313325
* 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).
326+
* (either in the form of Java 8's {@link java.util.Optional} or Kotlin's
327+
* nullable type).
315328
* @since 4.3
316329
*/
317330
public boolean isOptional() {
318-
return (getParameterType() == Optional.class || KotlinUtils.isNullable(this));
331+
return (getParameterType() == Optional.class ||
332+
(kotlinPresent && KotlinDelegate.isNullable(this)));
319333
}
320334

321335
/**
@@ -672,4 +686,40 @@ private static int validateIndex(Executable executable, int parameterIndex) {
672686
return parameterIndex;
673687
}
674688

689+
690+
/**
691+
* Inner class to avoid a hard dependency on Kotlin at runtime.
692+
*/
693+
private static class KotlinDelegate {
694+
695+
/**
696+
* Check whether the specified {@link MethodParameter} represents a nullable Kotlin type or not.
697+
*/
698+
public static boolean isNullable(MethodParameter param) {
699+
if (param.getContainingClass().isAnnotationPresent(Metadata.class)) {
700+
int parameterIndex = param.getParameterIndex();
701+
if (parameterIndex == -1) {
702+
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(param.getMethod());
703+
return (function != null && function.getReturnType().isMarkedNullable());
704+
}
705+
else {
706+
KFunction<?> function = (param.getMethod() != null ?
707+
ReflectJvmMapping.getKotlinFunction(param.getMethod()) :
708+
ReflectJvmMapping.getKotlinFunction(param.getConstructor()));
709+
if (function != null) {
710+
List<KParameter> parameters = function.getParameters();
711+
return parameters
712+
.stream()
713+
.filter(p -> KParameter.Kind.VALUE.equals(p.getKind()))
714+
.collect(Collectors.toList())
715+
.get(parameterIndex)
716+
.getType()
717+
.isMarkedNullable();
718+
}
719+
}
720+
}
721+
return false;
722+
}
723+
}
724+
675725
}

0 commit comments

Comments
 (0)