Browse Source

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
pull/1503/head
Sebastien Deleuze 9 years ago
parent
commit
ef68ccdbd8
  1. 63
      spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java
  2. 67
      spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java
  3. 108
      spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt

63
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.Set;
import java.util.concurrent.ConcurrentHashMap; 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.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@ -110,6 +114,7 @@ import org.springframework.util.StringUtils;
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Mark Fisher * @author Mark Fisher
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Sebastien Deleuze
* @since 2.5 * @since 2.5
* @see #setAutowiredAnnotationType * @see #setAutowiredAnnotationType
* @see Autowired * @see Autowired
@ -118,6 +123,22 @@ import org.springframework.util.StringUtils;
public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware { 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()); protected final Log logger = LogFactory.getLog(getClass());
private final Set<Class<? extends Annotation>> autowiredAnnotationTypes = new LinkedHashSet<>(); private final Set<Class<? extends Annotation>> autowiredAnnotationTypes = new LinkedHashSet<>();
@ -282,7 +303,14 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean
List<Constructor<?>> candidates = new ArrayList<Constructor<?>>(rawCandidates.length); List<Constructor<?>> candidates = new ArrayList<Constructor<?>>(rawCandidates.length);
Constructor<?> requiredConstructor = null; Constructor<?> requiredConstructor = null;
Constructor<?> defaultConstructor = null; Constructor<?> defaultConstructor = null;
Constructor<?> kotlinPrimaryConstructor = null;
if (useKotlinSupport(beanClass)) {
kotlinPrimaryConstructor = KotlinDelegate.findPrimaryConstructor(beanClass);
}
for (Constructor<?> candidate : rawCandidates) { for (Constructor<?> candidate : rawCandidates) {
if (kotlinPrimaryConstructor != null && candidate.isSynthetic()) {
continue;
}
AnnotationAttributes ann = findAutowiredAnnotation(candidate); AnnotationAttributes ann = findAutowiredAnnotation(candidate);
if (ann == null) { if (ann == null) {
Class<?> userClass = ClassUtils.getUserClass(beanClass); Class<?> userClass = ClassUtils.getUserClass(beanClass);
@ -338,6 +366,9 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean
else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) { else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {
candidateConstructors = new Constructor<?>[] {rawCandidates[0]}; candidateConstructors = new Constructor<?>[] {rawCandidates[0]};
} }
else if (kotlinPrimaryConstructor != null) {
candidateConstructors = new Constructor<?>[] {kotlinPrimaryConstructor};
}
else { else {
candidateConstructors = new Constructor<?>[0]; candidateConstructors = new Constructor<?>[0];
} }
@ -348,6 +379,15 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean
return (candidateConstructors.length > 0 ? candidateConstructors : null); 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<? extends Annotation>) kotlinMetadata) != null);
}
@Override @Override
public PropertyValues postProcessPropertyValues( public PropertyValues postProcessPropertyValues(
PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException { 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 <a href="http://kotlinlang.org/docs/reference/classes.html#constructors">http://kotlinlang.org/docs/reference/classes.html#constructors</a>
*/
@Nullable
public static <T> Constructor<T> findPrimaryConstructor(Class<T> clazz) {
KFunction<T> primaryConstructor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz));
if (primaryConstructor == null) {
return null;
}
Constructor<T> constructor = ReflectJvmMapping.getJavaConstructor(primaryConstructor);
Assert.notNull(constructor, "Can't get the Java constructor corresponding to the Kotlin primary constructor of " + clazz.getName());
return constructor;
}
}
} }

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

@ -17,6 +17,7 @@
package org.springframework.beans.factory.support; package org.springframework.beans.factory.support;
import java.beans.ConstructorProperties; import java.beans.ConstructorProperties;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Executable; import java.lang.reflect.Executable;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@ -31,6 +32,11 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; 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.BeanMetadataElement;
import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapper;
@ -65,6 +71,7 @@ import org.springframework.util.StringUtils;
* @author Rob Harrop * @author Rob Harrop
* @author Mark Fisher * @author Mark Fisher
* @author Costin Leau * @author Costin Leau
* @author Sebastien Deleuze
* @since 2.0 * @since 2.0
* @see #autowireConstructor * @see #autowireConstructor
* @see #instantiateUsingFactoryMethod * @see #instantiateUsingFactoryMethod
@ -75,6 +82,22 @@ class ConstructorResolver {
private static final NamedThreadLocal<InjectionPoint> currentInjectionPoint = private static final NamedThreadLocal<InjectionPoint> currentInjectionPoint =
new NamedThreadLocal<>("Current injection point"); 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; private final AbstractAutowireCapableBeanFactory beanFactory;
@ -805,10 +828,19 @@ class ConstructorResolver {
} }
return injectionPoint; return injectionPoint;
} }
boolean required = !(useKotlinSupport(param.getContainingClass()) && KotlinDelegate.isOptional(param));
return this.beanFactory.resolveDependency( 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<? extends Annotation>) kotlinMetadata) != null);
}
static InjectionPoint setCurrentInjectionPoint(@Nullable InjectionPoint injectionPoint) { static InjectionPoint setCurrentInjectionPoint(@Nullable InjectionPoint injectionPoint) {
InjectionPoint old = currentInjectionPoint.get(); 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<KParameter> parameters = function.getParameters();
return parameters
.stream()
.filter(p -> KParameter.Kind.VALUE.equals(p.getKind()))
.collect(Collectors.toList())
.get(index)
.isOptional();
}
return false;
}
}
} }

108
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.springframework.tests.sample.beans.TestBean
import org.junit.Assert.* import org.junit.Assert.*
import org.springframework.tests.sample.beans.Colour
/** /**
* Tests for Kotlin support with [Autowired]. * Tests for Kotlin support with [Autowired].
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sebastien Deleuze
*/ */
class KotlinAutowiredTests { class KotlinAutowiredTests {
@Test @Test
fun autowiringWithTarget() { fun `Autowiring with target`() {
var bf = DefaultListableBeanFactory() val bf = DefaultListableBeanFactory()
var bpp = AutowiredAnnotationBeanPostProcessor() val bpp = AutowiredAnnotationBeanPostProcessor()
bpp.setBeanFactory(bf) bpp.setBeanFactory(bf)
bf.addBeanPostProcessor(bpp) bf.addBeanPostProcessor(bpp)
var bd = RootBeanDefinition(KotlinBean::class.java) val bd = RootBeanDefinition(KotlinBean::class.java)
bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE
bf.registerBeanDefinition("annotatedBean", bd) bf.registerBeanDefinition("annotatedBean", bd)
var tb = TestBean() val tb = TestBean()
bf.registerSingleton("testBean", tb) 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.injectedFromConstructor)
assertSame(tb, kb.injectedFromMethod) assertSame(tb, kb.injectedFromMethod)
assertSame(tb, kb.injectedField) assertSame(tb, kb.injectedField)
} }
@Test @Test
fun autowiringWithoutTarget() { fun `Autowiring without target`() {
var bf = DefaultListableBeanFactory() val bf = DefaultListableBeanFactory()
var bpp = AutowiredAnnotationBeanPostProcessor() val bpp = AutowiredAnnotationBeanPostProcessor()
bpp.setBeanFactory(bf) bpp.setBeanFactory(bf)
bf.addBeanPostProcessor(bpp) bf.addBeanPostProcessor(bpp)
var bd = RootBeanDefinition(KotlinBean::class.java) val bd = RootBeanDefinition(KotlinBean::class.java)
bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE
bf.registerBeanDefinition("annotatedBean", bd) 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.injectedFromConstructor)
assertNull(kb.injectedFromMethod) assertNull(kb.injectedFromMethod)
assertNull(kb.injectedField) 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 var injectedFromMethod: TestBean? = null
@Autowired @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
}
} }

Loading…
Cancel
Save