Skip to content

Commit ef68ccd

Browse files
committed
Add support for Kotlin autowired ctors w/ optional params
This commit adds support for autowired constructor parameters on Kotlin classes with optional parameters. If some constructor parameters are not available, optional parameter default values will be used instead. Both explicit @Autowired annotated constructor and implicit single constructor automatically autowired are supported. Issue: SPR-15847
1 parent ab64305 commit ef68ccd

File tree

3 files changed

+225
-13
lines changed

3 files changed

+225
-13
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
import java.util.Set;
3535
import java.util.concurrent.ConcurrentHashMap;
3636

37+
import kotlin.jvm.JvmClassMappingKt;
38+
import kotlin.reflect.KFunction;
39+
import kotlin.reflect.full.KClasses;
40+
import kotlin.reflect.jvm.ReflectJvmMapping;
3741
import org.apache.commons.logging.Log;
3842
import org.apache.commons.logging.LogFactory;
3943

@@ -110,6 +114,7 @@
110114
* @author Juergen Hoeller
111115
* @author Mark Fisher
112116
* @author Stephane Nicoll
117+
* @author Sebastien Deleuze
113118
* @since 2.5
114119
* @see #setAutowiredAnnotationType
115120
* @see Autowired
@@ -118,6 +123,22 @@
118123
public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
119124
implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
120125

126+
@Nullable
127+
private static final Class<?> kotlinMetadata;
128+
129+
static {
130+
Class<?> metadata;
131+
try {
132+
metadata = ClassUtils.forName("kotlin.Metadata", AutowiredAnnotationBeanPostProcessor.class.getClassLoader());
133+
}
134+
catch (ClassNotFoundException ex) {
135+
// Kotlin API not available - no Kotlin support
136+
metadata = null;
137+
}
138+
kotlinMetadata = metadata;
139+
}
140+
141+
121142
protected final Log logger = LogFactory.getLog(getClass());
122143

123144
private final Set<Class<? extends Annotation>> autowiredAnnotationTypes = new LinkedHashSet<>();
@@ -282,7 +303,14 @@ public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final
282303
List<Constructor<?>> candidates = new ArrayList<Constructor<?>>(rawCandidates.length);
283304
Constructor<?> requiredConstructor = null;
284305
Constructor<?> defaultConstructor = null;
306+
Constructor<?> kotlinPrimaryConstructor = null;
307+
if (useKotlinSupport(beanClass)) {
308+
kotlinPrimaryConstructor = KotlinDelegate.findPrimaryConstructor(beanClass);
309+
}
285310
for (Constructor<?> candidate : rawCandidates) {
311+
if (kotlinPrimaryConstructor != null && candidate.isSynthetic()) {
312+
continue;
313+
}
286314
AnnotationAttributes ann = findAutowiredAnnotation(candidate);
287315
if (ann == null) {
288316
Class<?> userClass = ClassUtils.getUserClass(beanClass);
@@ -338,6 +366,9 @@ else if (candidates.size() == 1 && logger.isWarnEnabled()) {
338366
else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {
339367
candidateConstructors = new Constructor<?>[] {rawCandidates[0]};
340368
}
369+
else if (kotlinPrimaryConstructor != null) {
370+
candidateConstructors = new Constructor<?>[] {kotlinPrimaryConstructor};
371+
}
341372
else {
342373
candidateConstructors = new Constructor<?>[0];
343374
}
@@ -348,6 +379,15 @@ else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0)
348379
return (candidateConstructors.length > 0 ? candidateConstructors : null);
349380
}
350381

382+
/**
383+
* Return true if Kotlin is present and if the specified class is a Kotlin one.
384+
*/
385+
@SuppressWarnings("unchecked")
386+
private static boolean useKotlinSupport(Class<?> clazz) {
387+
return (kotlinMetadata != null &&
388+
clazz.getDeclaredAnnotation((Class<? extends Annotation>) kotlinMetadata) != null);
389+
}
390+
351391
@Override
352392
public PropertyValues postProcessPropertyValues(
353393
PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException {
@@ -729,4 +769,27 @@ public Object resolveShortcut(BeanFactory beanFactory) {
729769
}
730770
}
731771

772+
/**
773+
* Inner class to avoid a hard dependency on Kotlin at runtime.
774+
*/
775+
private static class KotlinDelegate {
776+
777+
/**
778+
* Return the Java constructor corresponding to the Kotlin primary constructor if any.
779+
* @param clazz the {@link Class} of the Kotlin class
780+
* @see <a href="http://kotlinlang.org/docs/reference/classes.html#constructors">http://kotlinlang.org/docs/reference/classes.html#constructors</a>
781+
*/
782+
@Nullable
783+
public static <T> Constructor<T> findPrimaryConstructor(Class<T> clazz) {
784+
KFunction<T> primaryConstructor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz));
785+
if (primaryConstructor == null) {
786+
return null;
787+
}
788+
Constructor<T> constructor = ReflectJvmMapping.getJavaConstructor(primaryConstructor);
789+
Assert.notNull(constructor, "Can't get the Java constructor corresponding to the Kotlin primary constructor of " + clazz.getName());
790+
return constructor;
791+
}
792+
793+
}
794+
732795
}

spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.beans.factory.support;
1818

1919
import java.beans.ConstructorProperties;
20+
import java.lang.annotation.Annotation;
2021
import java.lang.reflect.Constructor;
2122
import java.lang.reflect.Executable;
2223
import java.lang.reflect.Method;
@@ -31,6 +32,11 @@
3132
import java.util.List;
3233
import java.util.Map;
3334
import java.util.Set;
35+
import java.util.stream.Collectors;
36+
37+
import kotlin.reflect.KFunction;
38+
import kotlin.reflect.KParameter;
39+
import kotlin.reflect.jvm.ReflectJvmMapping;
3440

3541
import org.springframework.beans.BeanMetadataElement;
3642
import org.springframework.beans.BeanWrapper;
@@ -65,6 +71,7 @@
6571
* @author Rob Harrop
6672
* @author Mark Fisher
6773
* @author Costin Leau
74+
* @author Sebastien Deleuze
6875
* @since 2.0
6976
* @see #autowireConstructor
7077
* @see #instantiateUsingFactoryMethod
@@ -75,6 +82,22 @@ class ConstructorResolver {
7582
private static final NamedThreadLocal<InjectionPoint> currentInjectionPoint =
7683
new NamedThreadLocal<>("Current injection point");
7784

85+
@Nullable
86+
private static final Class<?> kotlinMetadata;
87+
88+
static {
89+
Class<?> metadata;
90+
try {
91+
metadata = ClassUtils.forName("kotlin.Metadata", ConstructorResolver.class.getClassLoader());
92+
}
93+
catch (ClassNotFoundException ex) {
94+
// Kotlin API not available - no Kotlin support
95+
metadata = null;
96+
}
97+
kotlinMetadata = metadata;
98+
}
99+
100+
78101
private final AbstractAutowireCapableBeanFactory beanFactory;
79102

80103

@@ -805,10 +828,19 @@ protected Object resolveAutowiredArgument(
805828
}
806829
return injectionPoint;
807830
}
831+
boolean required = !(useKotlinSupport(param.getContainingClass()) && KotlinDelegate.isOptional(param));
808832
return this.beanFactory.resolveDependency(
809-
new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter);
833+
new DependencyDescriptor(param, required), beanName, autowiredBeanNames, typeConverter);
810834
}
811835

836+
/**
837+
* Return true if Kotlin is present and if the specified class is a Kotlin one.
838+
*/
839+
@SuppressWarnings("unchecked")
840+
private static boolean useKotlinSupport(Class<?> clazz) {
841+
return (kotlinMetadata != null &&
842+
clazz.getDeclaredAnnotation((Class<? extends Annotation>) kotlinMetadata) != null);
843+
}
812844

813845
static InjectionPoint setCurrentInjectionPoint(@Nullable InjectionPoint injectionPoint) {
814846
InjectionPoint old = currentInjectionPoint.get();
@@ -915,4 +947,37 @@ public static String[] evaluate(Constructor<?> candidate, int paramCount) {
915947
}
916948
}
917949

950+
/**
951+
* Inner class to avoid a hard dependency on Kotlin at runtime.
952+
*/
953+
private static class KotlinDelegate {
954+
955+
/**
956+
* Check whether the specified {@link MethodParameter} represents an optional Kotlin parameter or not.
957+
*/
958+
public static boolean isOptional(MethodParameter param) {
959+
Method method = param.getMethod();
960+
Constructor<?> ctor = param.getConstructor();
961+
int index = param.getParameterIndex();
962+
KFunction<?> function = null;
963+
if (method != null) {
964+
function = ReflectJvmMapping.getKotlinFunction(method);
965+
}
966+
else if (ctor != null) {
967+
function = ReflectJvmMapping.getKotlinFunction(ctor);
968+
}
969+
if (function != null) {
970+
List<KParameter> parameters = function.getParameters();
971+
return parameters
972+
.stream()
973+
.filter(p -> KParameter.Kind.VALUE.equals(p.getKind()))
974+
.collect(Collectors.toList())
975+
.get(index)
976+
.isOptional();
977+
}
978+
return false;
979+
}
980+
981+
}
982+
918983
}

spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,51 +23,108 @@ import org.springframework.beans.factory.support.RootBeanDefinition
2323
import org.springframework.tests.sample.beans.TestBean
2424

2525
import org.junit.Assert.*
26+
import org.springframework.tests.sample.beans.Colour
2627

2728
/**
2829
* Tests for Kotlin support with [Autowired].
2930
*
3031
* @author Juergen Hoeller
32+
* @author Sebastien Deleuze
3133
*/
3234
class KotlinAutowiredTests {
3335

3436
@Test
35-
fun autowiringWithTarget() {
36-
var bf = DefaultListableBeanFactory()
37-
var bpp = AutowiredAnnotationBeanPostProcessor()
37+
fun `Autowiring with target`() {
38+
val bf = DefaultListableBeanFactory()
39+
val bpp = AutowiredAnnotationBeanPostProcessor()
3840
bpp.setBeanFactory(bf)
3941
bf.addBeanPostProcessor(bpp)
40-
var bd = RootBeanDefinition(KotlinBean::class.java)
42+
val bd = RootBeanDefinition(KotlinBean::class.java)
4143
bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE
4244
bf.registerBeanDefinition("annotatedBean", bd)
43-
var tb = TestBean()
45+
val tb = TestBean()
4446
bf.registerSingleton("testBean", tb)
4547

46-
var kb = bf.getBean("annotatedBean", KotlinBean::class.java)
48+
val kb = bf.getBean("annotatedBean", KotlinBean::class.java)
4749
assertSame(tb, kb.injectedFromConstructor)
4850
assertSame(tb, kb.injectedFromMethod)
4951
assertSame(tb, kb.injectedField)
5052
}
5153

5254
@Test
53-
fun autowiringWithoutTarget() {
54-
var bf = DefaultListableBeanFactory()
55-
var bpp = AutowiredAnnotationBeanPostProcessor()
55+
fun `Autowiring without target`() {
56+
val bf = DefaultListableBeanFactory()
57+
val bpp = AutowiredAnnotationBeanPostProcessor()
5658
bpp.setBeanFactory(bf)
5759
bf.addBeanPostProcessor(bpp)
58-
var bd = RootBeanDefinition(KotlinBean::class.java)
60+
val bd = RootBeanDefinition(KotlinBean::class.java)
5961
bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE
6062
bf.registerBeanDefinition("annotatedBean", bd)
6163

62-
var kb = bf.getBean("annotatedBean", KotlinBean::class.java)
64+
val kb = bf.getBean("annotatedBean", KotlinBean::class.java)
6365
assertNull(kb.injectedFromConstructor)
6466
assertNull(kb.injectedFromMethod)
6567
assertNull(kb.injectedField)
6668
}
69+
70+
@Test // SPR-15847
71+
fun `Autowiring by primary constructor with optional parameter`() {
72+
val bf = DefaultListableBeanFactory()
73+
val bpp = AutowiredAnnotationBeanPostProcessor()
74+
bpp.setBeanFactory(bf)
75+
bf.addBeanPostProcessor(bpp)
76+
val bd = RootBeanDefinition(KotlinBeanWithOptionalParameter::class.java)
77+
bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE
78+
bf.registerBeanDefinition("bean", bd)
79+
val tb = TestBean()
80+
bf.registerSingleton("testBean", tb)
6781

82+
val kb = bf.getBean("bean", KotlinBeanWithOptionalParameter::class.java)
83+
assertSame(tb, kb.injectedFromConstructor)
84+
assertEquals("foo", kb.optional)
85+
assertEquals("bar", kb.initializedField)
86+
}
6887

69-
class KotlinBean(val injectedFromConstructor: TestBean?) {
88+
@Test // SPR-15847
89+
fun `Autowiring by annotated primary constructor with optional parameter`() {
90+
val bf = DefaultListableBeanFactory()
91+
val bpp = AutowiredAnnotationBeanPostProcessor()
92+
bpp.setBeanFactory(bf)
93+
bf.addBeanPostProcessor(bpp)
94+
val bd = RootBeanDefinition(KotlinBeanWithOptionalParameterAndExplicitConstructor::class.java)
95+
bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE
96+
bf.registerBeanDefinition("bean", bd)
97+
val tb = TestBean()
98+
bf.registerSingleton("testBean", tb)
99+
100+
val kb = bf.getBean("bean", KotlinBeanWithOptionalParameterAndExplicitConstructor::class.java)
101+
assertSame(tb, kb.injectedFromConstructor)
102+
assertEquals("foo", kb.optional)
103+
}
70104

105+
@Test // SPR-15847
106+
fun `Autowiring by annotated secondary constructor with optional parameter`() {
107+
val bf = DefaultListableBeanFactory()
108+
val bpp = AutowiredAnnotationBeanPostProcessor()
109+
bpp.setBeanFactory(bf)
110+
bf.addBeanPostProcessor(bpp)
111+
val bd = RootBeanDefinition(KotlinBeanWithSecondaryConstructor::class.java)
112+
bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE
113+
bf.registerBeanDefinition("bean", bd)
114+
val tb = TestBean()
115+
bf.registerSingleton("testBean", tb)
116+
val colour = Colour.BLUE
117+
bf.registerSingleton("colour", colour)
118+
119+
val kb = bf.getBean("bean", KotlinBeanWithSecondaryConstructor::class.java)
120+
assertSame(tb, kb.injectedFromConstructor)
121+
assertEquals("bar", kb.optional)
122+
assertSame(colour, kb.injectedFromSecondaryConstructor)
123+
}
124+
125+
126+
class KotlinBean(val injectedFromConstructor: TestBean?) {
127+
71128
var injectedFromMethod: TestBean? = null
72129

73130
@Autowired
@@ -79,4 +136,31 @@ class KotlinAutowiredTests {
79136
}
80137
}
81138

139+
class KotlinBeanWithOptionalParameter(
140+
val injectedFromConstructor: TestBean,
141+
val optional: String = "foo"
142+
) {
143+
var initializedField: String? = null
144+
145+
init {
146+
initializedField = "bar"
147+
}
148+
}
149+
150+
class KotlinBeanWithOptionalParameterAndExplicitConstructor @Autowired constructor(
151+
val optional: String = "foo",
152+
val injectedFromConstructor: TestBean
153+
)
154+
155+
class KotlinBeanWithSecondaryConstructor(
156+
val optional: String = "foo",
157+
val injectedFromConstructor: TestBean
158+
) {
159+
@Autowired constructor(injectedFromSecondaryConstructor: Colour, injectedFromConstructor: TestBean, optional: String = "bar") : this(optional, injectedFromConstructor) {
160+
this.injectedFromSecondaryConstructor = injectedFromSecondaryConstructor
161+
}
162+
163+
var injectedFromSecondaryConstructor: Colour? = null
164+
}
165+
82166
}

0 commit comments

Comments
 (0)