From ef68ccdbd85c4662ef0634729250e35d7e7fde83 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 22 Aug 2017 16:46:43 +0200 Subject: [PATCH] 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 --- .../AutowiredAnnotationBeanPostProcessor.java | 63 ++++++++++ .../factory/support/ConstructorResolver.java | 67 ++++++++++- .../annotation/KotlinAutowiredTests.kt | 108 ++++++++++++++++-- 3 files changed, 225 insertions(+), 13 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index a3339b3f822..9bd70bbc8d2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -34,6 +34,10 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import kotlin.jvm.JvmClassMappingKt; +import kotlin.reflect.KFunction; +import kotlin.reflect.full.KClasses; +import kotlin.reflect.jvm.ReflectJvmMapping; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -110,6 +114,7 @@ import org.springframework.util.StringUtils; * @author Juergen Hoeller * @author Mark Fisher * @author Stephane Nicoll + * @author Sebastien Deleuze * @since 2.5 * @see #setAutowiredAnnotationType * @see Autowired @@ -118,6 +123,22 @@ import org.springframework.util.StringUtils; public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware { + @Nullable + private static final Class kotlinMetadata; + + static { + Class metadata; + try { + metadata = ClassUtils.forName("kotlin.Metadata", AutowiredAnnotationBeanPostProcessor.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // Kotlin API not available - no Kotlin support + metadata = null; + } + kotlinMetadata = metadata; + } + + protected final Log logger = LogFactory.getLog(getClass()); private final Set> autowiredAnnotationTypes = new LinkedHashSet<>(); @@ -282,7 +303,14 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean List> candidates = new ArrayList>(rawCandidates.length); Constructor requiredConstructor = null; Constructor defaultConstructor = null; + Constructor kotlinPrimaryConstructor = null; + if (useKotlinSupport(beanClass)) { + kotlinPrimaryConstructor = KotlinDelegate.findPrimaryConstructor(beanClass); + } for (Constructor candidate : rawCandidates) { + if (kotlinPrimaryConstructor != null && candidate.isSynthetic()) { + continue; + } AnnotationAttributes ann = findAutowiredAnnotation(candidate); if (ann == null) { Class userClass = ClassUtils.getUserClass(beanClass); @@ -338,6 +366,9 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) { candidateConstructors = new Constructor[] {rawCandidates[0]}; } + else if (kotlinPrimaryConstructor != null) { + candidateConstructors = new Constructor[] {kotlinPrimaryConstructor}; + } else { candidateConstructors = new Constructor[0]; } @@ -348,6 +379,15 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean return (candidateConstructors.length > 0 ? candidateConstructors : null); } + /** + * 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); + } + @Override public PropertyValues postProcessPropertyValues( PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException { @@ -729,4 +769,27 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean } } + /** + * Inner class to avoid a hard dependency on Kotlin at runtime. + */ + private static class KotlinDelegate { + + /** + * Return the Java constructor corresponding to the Kotlin primary constructor if any. + * @param clazz the {@link Class} of the Kotlin class + * @see http://kotlinlang.org/docs/reference/classes.html#constructors + */ + @Nullable + public static Constructor findPrimaryConstructor(Class clazz) { + KFunction primaryConstructor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz)); + if (primaryConstructor == null) { + return null; + } + Constructor constructor = ReflectJvmMapping.getJavaConstructor(primaryConstructor); + Assert.notNull(constructor, "Can't get the Java constructor corresponding to the Kotlin primary constructor of " + clazz.getName()); + return constructor; + } + + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index aefb66f89f0..17526e763fa 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -17,6 +17,7 @@ package org.springframework.beans.factory.support; import java.beans.ConstructorProperties; +import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; @@ -31,6 +32,11 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; + +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.jvm.ReflectJvmMapping; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.BeanWrapper; @@ -65,6 +71,7 @@ import org.springframework.util.StringUtils; * @author Rob Harrop * @author Mark Fisher * @author Costin Leau + * @author Sebastien Deleuze * @since 2.0 * @see #autowireConstructor * @see #instantiateUsingFactoryMethod @@ -75,6 +82,22 @@ class ConstructorResolver { private static final NamedThreadLocal currentInjectionPoint = new NamedThreadLocal<>("Current injection point"); + @Nullable + private static final Class kotlinMetadata; + + static { + Class metadata; + try { + metadata = ClassUtils.forName("kotlin.Metadata", ConstructorResolver.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // Kotlin API not available - no Kotlin support + metadata = null; + } + kotlinMetadata = metadata; + } + + private final AbstractAutowireCapableBeanFactory beanFactory; @@ -805,10 +828,19 @@ class ConstructorResolver { } return injectionPoint; } + boolean required = !(useKotlinSupport(param.getContainingClass()) && KotlinDelegate.isOptional(param)); return this.beanFactory.resolveDependency( - new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter); + new DependencyDescriptor(param, required), beanName, autowiredBeanNames, typeConverter); } + /** + * 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); + } static InjectionPoint setCurrentInjectionPoint(@Nullable InjectionPoint injectionPoint) { InjectionPoint old = currentInjectionPoint.get(); @@ -915,4 +947,37 @@ class ConstructorResolver { } } + /** + * Inner class to avoid a hard dependency on Kotlin at runtime. + */ + private static class KotlinDelegate { + + /** + * Check whether the specified {@link MethodParameter} represents an optional Kotlin parameter or not. + */ + public static boolean isOptional(MethodParameter param) { + Method method = param.getMethod(); + Constructor ctor = param.getConstructor(); + int index = param.getParameterIndex(); + KFunction function = null; + if (method != null) { + function = ReflectJvmMapping.getKotlinFunction(method); + } + else if (ctor != null) { + function = ReflectJvmMapping.getKotlinFunction(ctor); + } + if (function != null) { + List parameters = function.getParameters(); + return parameters + .stream() + .filter(p -> KParameter.Kind.VALUE.equals(p.getKind())) + .collect(Collectors.toList()) + .get(index) + .isOptional(); + } + return false; + } + + } + } diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt index 2b2f3d67e5b..e64100c6507 100644 --- a/spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt +++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt @@ -23,51 +23,108 @@ import org.springframework.beans.factory.support.RootBeanDefinition import org.springframework.tests.sample.beans.TestBean import org.junit.Assert.* +import org.springframework.tests.sample.beans.Colour /** * Tests for Kotlin support with [Autowired]. * * @author Juergen Hoeller + * @author Sebastien Deleuze */ class KotlinAutowiredTests { @Test - fun autowiringWithTarget() { - var bf = DefaultListableBeanFactory() - var bpp = AutowiredAnnotationBeanPostProcessor() + fun `Autowiring with target`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() bpp.setBeanFactory(bf) bf.addBeanPostProcessor(bpp) - var bd = RootBeanDefinition(KotlinBean::class.java) + val bd = RootBeanDefinition(KotlinBean::class.java) bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE bf.registerBeanDefinition("annotatedBean", bd) - var tb = TestBean() + val tb = TestBean() bf.registerSingleton("testBean", tb) - var kb = bf.getBean("annotatedBean", KotlinBean::class.java) + val kb = bf.getBean("annotatedBean", KotlinBean::class.java) assertSame(tb, kb.injectedFromConstructor) assertSame(tb, kb.injectedFromMethod) assertSame(tb, kb.injectedField) } @Test - fun autowiringWithoutTarget() { - var bf = DefaultListableBeanFactory() - var bpp = AutowiredAnnotationBeanPostProcessor() + fun `Autowiring without target`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() bpp.setBeanFactory(bf) bf.addBeanPostProcessor(bpp) - var bd = RootBeanDefinition(KotlinBean::class.java) + val bd = RootBeanDefinition(KotlinBean::class.java) bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE bf.registerBeanDefinition("annotatedBean", bd) - var kb = bf.getBean("annotatedBean", KotlinBean::class.java) + val kb = bf.getBean("annotatedBean", KotlinBean::class.java) assertNull(kb.injectedFromConstructor) assertNull(kb.injectedFromMethod) assertNull(kb.injectedField) } + + @Test // SPR-15847 + fun `Autowiring by primary constructor with optional parameter`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + val bd = RootBeanDefinition(KotlinBeanWithOptionalParameter::class.java) + bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE + bf.registerBeanDefinition("bean", bd) + val tb = TestBean() + bf.registerSingleton("testBean", tb) + val kb = bf.getBean("bean", KotlinBeanWithOptionalParameter::class.java) + assertSame(tb, kb.injectedFromConstructor) + assertEquals("foo", kb.optional) + assertEquals("bar", kb.initializedField) + } - class KotlinBean(val injectedFromConstructor: TestBean?) { + @Test // SPR-15847 + fun `Autowiring by annotated primary constructor with optional parameter`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + val bd = RootBeanDefinition(KotlinBeanWithOptionalParameterAndExplicitConstructor::class.java) + bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE + bf.registerBeanDefinition("bean", bd) + val tb = TestBean() + bf.registerSingleton("testBean", tb) + + val kb = bf.getBean("bean", KotlinBeanWithOptionalParameterAndExplicitConstructor::class.java) + assertSame(tb, kb.injectedFromConstructor) + assertEquals("foo", kb.optional) + } + @Test // SPR-15847 + fun `Autowiring by annotated secondary constructor with optional parameter`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + val bd = RootBeanDefinition(KotlinBeanWithSecondaryConstructor::class.java) + bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE + bf.registerBeanDefinition("bean", bd) + val tb = TestBean() + bf.registerSingleton("testBean", tb) + val colour = Colour.BLUE + bf.registerSingleton("colour", colour) + + val kb = bf.getBean("bean", KotlinBeanWithSecondaryConstructor::class.java) + assertSame(tb, kb.injectedFromConstructor) + assertEquals("bar", kb.optional) + assertSame(colour, kb.injectedFromSecondaryConstructor) + } + + + class KotlinBean(val injectedFromConstructor: TestBean?) { + var injectedFromMethod: TestBean? = null @Autowired @@ -79,4 +136,31 @@ class KotlinAutowiredTests { } } + class KotlinBeanWithOptionalParameter( + val injectedFromConstructor: TestBean, + val optional: String = "foo" + ) { + var initializedField: String? = null + + init { + initializedField = "bar" + } + } + + class KotlinBeanWithOptionalParameterAndExplicitConstructor @Autowired constructor( + val optional: String = "foo", + val injectedFromConstructor: TestBean + ) + + class KotlinBeanWithSecondaryConstructor( + val optional: String = "foo", + val injectedFromConstructor: TestBean + ) { + @Autowired constructor(injectedFromSecondaryConstructor: Colour, injectedFromConstructor: TestBean, optional: String = "bar") : this(optional, injectedFromConstructor) { + this.injectedFromSecondaryConstructor = injectedFromSecondaryConstructor + } + + var injectedFromSecondaryConstructor: Colour? = null + } + }