Skip to content

Commit fa4d139

Browse files
committed
Support instantiating Kotlin classes with optional parameters
This commit updates BeanUtils class in order to add Kotlin optional parameters with default values support to the immutable data classes support introduced by SPR-15199. Issue: SPR-15673
1 parent 5cac619 commit fa4d139

File tree

7 files changed

+316
-14
lines changed

7 files changed

+316
-14
lines changed

build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@ project("spring-context") {
517517
optional("org.aspectj:aspectjweaver:${aspectjVersion}")
518518
optional("org.codehaus.groovy:groovy-all:${groovyVersion}")
519519
optional("org.beanshell:bsh:2.0b5")
520+
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
520521
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
521522
testCompile("org.apache.commons:commons-pool2:2.4.2")
522523
testCompile("org.slf4j:slf4j-api:${slf4jVersion}")
@@ -755,6 +756,7 @@ project("spring-web") {
755756
optional("javax.xml.bind:jaxb-api:${jaxbVersion}")
756757
optional("javax.xml.ws:jaxws-api:${jaxwsVersion}")
757758
optional("javax.mail:javax.mail-api:${javamailVersion}")
759+
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
758760
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
759761
testCompile("io.projectreactor:reactor-test")
760762
testCompile("org.apache.taglibs:taglibs-standard-jstlel:1.2.1") {
@@ -853,6 +855,8 @@ project("spring-webmvc") {
853855
}
854856
optional('org.webjars:webjars-locator:0.32-1')
855857
optional("org.reactivestreams:reactive-streams")
858+
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
859+
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
856860
testCompile("org.xmlunit:xmlunit-matchers:${xmlunitVersion}")
857861
testCompile("dom4j:dom4j:1.6.1") {
858862
exclude group: "xml-apis", module: "xml-apis"
@@ -1048,6 +1052,7 @@ project("spring-test") {
10481052
optional("org.reactivestreams:reactive-streams")
10491053
optional("io.projectreactor:reactor-core")
10501054
optional("io.projectreactor:reactor-test")
1055+
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
10511056
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
10521057
testCompile(project(":spring-context-support"))
10531058
testCompile(project(":spring-oxm"))

spring-beans/src/main/java/org/springframework/beans/BeanUtils.java

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.beans.PropertyDescriptor;
2020
import java.beans.PropertyEditor;
21+
import java.lang.annotation.Annotation;
2122
import java.lang.reflect.Constructor;
2223
import java.lang.reflect.InvocationTargetException;
2324
import java.lang.reflect.Method;
@@ -27,10 +28,17 @@
2728
import java.util.Arrays;
2829
import java.util.Collections;
2930
import java.util.Date;
31+
import java.util.HashMap;
3032
import java.util.List;
3133
import java.util.Locale;
34+
import java.util.Map;
3235
import java.util.Set;
3336

37+
import kotlin.jvm.JvmClassMappingKt;
38+
import kotlin.reflect.KFunction;
39+
import kotlin.reflect.KParameter;
40+
import kotlin.reflect.full.KClasses;
41+
import kotlin.reflect.jvm.ReflectJvmMapping;
3442
import org.apache.commons.logging.Log;
3543
import org.apache.commons.logging.LogFactory;
3644

@@ -53,6 +61,7 @@
5361
* @author Juergen Hoeller
5462
* @author Rob Harrop
5563
* @author Sam Brannen
64+
* @author Sebastien Deleuze
5665
*/
5766
public abstract class BeanUtils {
5867

@@ -61,6 +70,9 @@ public abstract class BeanUtils {
6170
private static final Set<Class<?>> unknownEditorTypes =
6271
Collections.newSetFromMap(new ConcurrentReferenceHashMap<>(64));
6372

73+
private static final boolean kotlinPresent =
74+
ClassUtils.isPresent("kotlin.Unit", BeanUtils.class.getClassLoader());
75+
6476

6577
/**
6678
* Convenience method to instantiate a class using its no-arg constructor.
@@ -103,7 +115,12 @@ public static <T> T instantiateClass(Class<T> clazz) throws BeanInstantiationExc
103115
throw new BeanInstantiationException(clazz, "Specified class is an interface");
104116
}
105117
try {
106-
return instantiateClass(clazz.getDeclaredConstructor());
118+
Constructor<T> ctor = (kotlinPresent && isKotlinClass(clazz) ?
119+
KotlinDelegate.findPrimaryConstructor(clazz) : clazz.getDeclaredConstructor());
120+
if (ctor == null) {
121+
throw new BeanInstantiationException(clazz, "No default constructor found");
122+
}
123+
return instantiateClass(ctor);
107124
}
108125
catch (NoSuchMethodException ex) {
109126
throw new BeanInstantiationException(clazz, "No default constructor found", ex);
@@ -132,9 +149,11 @@ public static <T> T instantiateClass(Class<?> clazz, Class<T> assignableTo) thro
132149
/**
133150
* Convenience method to instantiate a class using the given constructor.
134151
* <p>Note that this method tries to set the constructor accessible if given a
135-
* non-accessible (that is, non-public) constructor.
152+
* non-accessible (that is, non-public) constructor, and supports Kotlin classes
153+
* with optional parameters and default values.
136154
* @param ctor the constructor to instantiate
137-
* @param args the constructor arguments to apply
155+
* @param args the constructor arguments to apply (use null for unspecified parameter
156+
* if needed for Kotlin classes with optional parameters and default values)
138157
* @return the new instance
139158
* @throws BeanInstantiationException if the bean cannot be instantiated
140159
* @see Constructor#newInstance
@@ -143,7 +162,7 @@ public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws
143162
Assert.notNull(ctor, "Constructor must not be null");
144163
try {
145164
ReflectionUtils.makeAccessible(ctor);
146-
return ctor.newInstance(args);
165+
return (kotlinPresent && isKotlinClass(ctor.getDeclaringClass()) ? KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args));
147166
}
148167
catch (InstantiationException ex) {
149168
throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex);
@@ -299,6 +318,37 @@ else if (!method.isBridge() && targetMethod.getParameterCount() == numParams) {
299318
return targetMethod;
300319
}
301320

321+
/**
322+
* Return the primary constructor of the provided class (single or default constructor
323+
* for Java classes and primary constructor for Kotlin classes) if any.
324+
* @param clazz the {@link Class} of the Kotlin class
325+
* @see <a href="http://kotlinlang.org/docs/reference/classes.html#constructors">http://kotlinlang.org/docs/reference/classes.html#constructors</a>
326+
* @since 5.0
327+
*/
328+
@SuppressWarnings("unchecked")
329+
@Nullable
330+
public static <T> Constructor<T> findPrimaryConstructor(Class<T> clazz) {
331+
Assert.notNull(clazz, "Class must not be null");
332+
Constructor<T> ctor = null;
333+
if (kotlinPresent && isKotlinClass(clazz)) {
334+
ctor = KotlinDelegate.findPrimaryConstructor(clazz);
335+
}
336+
else {
337+
Constructor<T>[] ctors = (Constructor<T>[])clazz.getConstructors();
338+
if (ctors.length == 1) {
339+
ctor = ctors[0];
340+
}
341+
else {
342+
try {
343+
ctor = clazz.getDeclaredConstructor();
344+
}
345+
catch (NoSuchMethodException e) {
346+
}
347+
}
348+
}
349+
return ctor;
350+
}
351+
302352
/**
303353
* Parse a method signature in the form {@code methodName[([arg_list])]},
304354
* where {@code arg_list} is an optional, comma-separated list of fully-qualified
@@ -646,4 +696,63 @@ private static void copyProperties(Object source, Object target, @Nullable Class
646696
}
647697
}
648698

699+
/**
700+
* Return true if the specified class is a Kotlin one.
701+
*/
702+
private static boolean isKotlinClass(Class<?> clazz) {
703+
for (Annotation annotation : clazz.getDeclaredAnnotations()) {
704+
if (annotation.annotationType().getName().equals("kotlin.Metadata")) {
705+
return true;
706+
}
707+
}
708+
return false;
709+
}
710+
711+
712+
/**
713+
* Inner class to avoid a hard dependency on Kotlin at runtime.
714+
*/
715+
private static class KotlinDelegate {
716+
717+
/**
718+
* Return the Java constructor corresponding to the Kotlin primary constructor if any.
719+
* @param clazz the {@link Class} of the Kotlin class
720+
* @see <a href="http://kotlinlang.org/docs/reference/classes.html#constructors">http://kotlinlang.org/docs/reference/classes.html#constructors</a>
721+
*/
722+
@Nullable
723+
public static <T> Constructor<T> findPrimaryConstructor(Class<T> clazz) {
724+
KFunction<T> primaryConstructor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz));
725+
if (primaryConstructor == null) {
726+
return null;
727+
}
728+
Constructor<T> constructor = ReflectJvmMapping.getJavaConstructor(primaryConstructor);
729+
Assert.notNull(constructor, "Can't get the Java constructor corresponding to the Kotlin primary constructor of " + clazz.getName());
730+
return constructor;
731+
}
732+
733+
/**
734+
* Instantiate a Kotlin class using the provided constructor.
735+
* @param ctor the constructor of the Kotlin class to instantiate
736+
* @param args the constructor arguments to apply (use null for unspecified parameter if needed)
737+
* @throws BeanInstantiationException if no primary constructor can be found
738+
*/
739+
public static <T> T instantiateClass(Constructor<T> ctor, Object... args) {
740+
KFunction<T> kotlinConstructor = ReflectJvmMapping.getKotlinFunction(ctor);
741+
if (kotlinConstructor == null) {
742+
throw new BeanInstantiationException(ctor.getDeclaringClass(), "No corresponding Kotlin constructor found");
743+
}
744+
List<KParameter> parameters = kotlinConstructor.getParameters();
745+
Map<KParameter, Object> argParameters = new HashMap<>(parameters.size());
746+
Assert.isTrue(args.length <= parameters.size(),
747+
"The number of provided arguments should be less of equals than the number of constructor parameters");
748+
for (int i = 0 ; i < args.length ; i++) {
749+
if (!(parameters.get(i).isOptional() && (args[i] == null))) {
750+
argParameters.put(parameters.get(i), args[i]);
751+
}
752+
}
753+
return kotlinConstructor.callBy(argParameters);
754+
}
755+
756+
}
757+
649758
}

spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.beans.Introspector;
2020
import java.beans.PropertyDescriptor;
21+
import java.lang.reflect.Constructor;
2122
import java.lang.reflect.Method;
2223
import java.util.ArrayList;
2324
import java.util.List;
@@ -274,6 +275,23 @@ public void testSPR6063() {
274275
}
275276
}
276277
}
278+
279+
@Test
280+
public void testFindDefaultConstructorAndInstantiate() {
281+
Constructor<Bean> ctor = BeanUtils.findPrimaryConstructor(Bean.class);
282+
assertNotNull(ctor);
283+
Bean bean = BeanUtils.instantiateClass(ctor);
284+
assertNotNull(bean);
285+
}
286+
287+
@Test
288+
public void testFindSingleNonDefaultConstructorAndInstantiate() {
289+
Constructor<BeanWithSingleNonDefaultConstructor> ctor = BeanUtils.findPrimaryConstructor(BeanWithSingleNonDefaultConstructor.class);
290+
assertNotNull(ctor);
291+
BeanWithSingleNonDefaultConstructor bean = BeanUtils.instantiateClass(ctor, "foo");
292+
assertNotNull(bean);
293+
assertEquals("foo", bean.getName());
294+
}
277295

278296
private void assertSignatureEquals(Method desiredMethod, String signature) {
279297
assertEquals(desiredMethod, BeanUtils.resolveSignature(signature, MethodSignatureBean.class));
@@ -444,5 +462,18 @@ public void setValue(String aValue) {
444462
value = aValue;
445463
}
446464
}
465+
466+
private static class BeanWithSingleNonDefaultConstructor {
467+
468+
private final String name;
469+
470+
public BeanWithSingleNonDefaultConstructor(String name) {
471+
this.name = name;
472+
}
473+
474+
public String getName() {
475+
return name;
476+
}
477+
}
447478

448479
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.beans
18+
19+
import org.junit.Assert.*
20+
import org.junit.Test
21+
22+
/**
23+
* Kotlin tests for {@link BeanUtils}
24+
*
25+
* @author Sebastien Deleuze
26+
*/
27+
class BeanUtilsKotlinTests {
28+
29+
@Test
30+
fun `Instantiate immutable class`() {
31+
val constructor = BeanUtils.findPrimaryConstructor(Foo::class.java)
32+
val foo = BeanUtils.instantiateClass(constructor, "bar", 3) as Foo
33+
assertEquals("bar", foo.param1)
34+
assertEquals(3, foo.param2)
35+
}
36+
37+
@Test
38+
fun `Instantiate immutable class with optional parameter and all parameters specified`() {
39+
val constructor = BeanUtils.findPrimaryConstructor(Bar::class.java)
40+
val bar = BeanUtils.instantiateClass(constructor, "baz", 8) as Bar
41+
assertEquals("baz", bar.param1)
42+
assertEquals(8, bar.param2)
43+
}
44+
45+
@Test
46+
fun `Instantiate immutable class with optional parameter and only mandatory parameters specified by position`() {
47+
val constructor = BeanUtils.findPrimaryConstructor(Bar::class.java)
48+
val bar = BeanUtils.instantiateClass(constructor, "baz") as Bar
49+
assertEquals("baz", bar.param1)
50+
assertEquals(12, bar.param2)
51+
}
52+
53+
@Test
54+
fun `Instantiate immutable class with optional parameter specified with null value`() {
55+
val constructor = BeanUtils.findPrimaryConstructor(Bar::class.java)
56+
val bar = BeanUtils.instantiateClass(constructor, "baz", null) as Bar
57+
assertEquals("baz", bar.param1)
58+
assertEquals(12, bar.param2)
59+
}
60+
61+
class Foo(val param1: String, val param2: Int)
62+
63+
class Bar(val param1: String, val param2: Int = 12)
64+
65+
}

spring-core/src/main/java/org/springframework/core/MethodParameter.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import java.util.Optional;
3232
import java.util.stream.Collectors;
3333

34-
import kotlin.Metadata;
3534
import kotlin.reflect.KFunction;
3635
import kotlin.reflect.KParameter;
3736
import kotlin.reflect.jvm.ReflectJvmMapping;
@@ -737,7 +736,7 @@ private static class KotlinDelegate {
737736
* Check whether the specified {@link MethodParameter} represents a nullable Kotlin type or not.
738737
*/
739738
public static boolean isNullable(MethodParameter param) {
740-
if (param.getContainingClass().isAnnotationPresent(Metadata.class)) {
739+
if (isKotlinClass(param.getContainingClass())) {
741740
Method method = param.getMethod();
742741
Constructor<?> ctor = param.getConstructor();
743742
int index = param.getParameterIndex();
@@ -767,6 +766,16 @@ else if (ctor != null) {
767766
}
768767
return false;
769768
}
769+
770+
private static boolean isKotlinClass(Class<?> clazz) {
771+
for (Annotation annotation : clazz.getDeclaredAnnotations()) {
772+
if (annotation.annotationType().getName().equals("kotlin.Metadata")) {
773+
return true;
774+
}
775+
}
776+
return false;
777+
}
778+
770779
}
771780

772781
}

0 commit comments

Comments
 (0)