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 + } + }