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; @@ -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; @@ -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; @@ -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<Class<? extends Annotation>> autowiredAnnotationTypes = new LinkedHashSet<>();
@ -282,7 +303,14 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean @@ -282,7 +303,14 @@ public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBean
List<Constructor<?>> candidates = new ArrayList<Constructor<?>>(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 @@ -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 @@ -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<? extends Annotation>) 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 @@ -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 @@ @@ -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; @@ -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; @@ -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 { @@ -75,6 +82,22 @@ class ConstructorResolver {
private static final NamedThreadLocal<InjectionPoint> 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 { @@ -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<? extends Annotation>) kotlinMetadata) != null);
}
static InjectionPoint setCurrentInjectionPoint(@Nullable InjectionPoint injectionPoint) {
InjectionPoint old = currentInjectionPoint.get();
@ -915,4 +947,37 @@ class ConstructorResolver { @@ -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 @@ -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 { @@ -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