Skip to content

Commit a03a147

Browse files
committed
Add AOT support for Kotlin constructors with optional parameters
This commit leverages Kotlin reflection to instantiate classes with constructors using optional parameters in the code generated AOT. Closes gh-29820
1 parent 20dd66c commit a03a147

File tree

5 files changed

+223
-8
lines changed

5 files changed

+223
-8
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import org.springframework.aot.hint.ExecutableMode;
2929
import org.springframework.beans.BeanInstantiationException;
30+
import org.springframework.beans.BeanUtils;
3031
import org.springframework.beans.BeansException;
3132
import org.springframework.beans.TypeConverter;
3233
import org.springframework.beans.factory.BeanFactory;
@@ -343,8 +344,7 @@ private Object instantiate(Constructor<?> constructor, Object[] args) throws Exc
343344
Object enclosingInstance = createInstance(declaringClass.getEnclosingClass());
344345
args = ObjectUtils.addObjectToArray(args, enclosingInstance, 0);
345346
}
346-
ReflectionUtils.makeAccessible(constructor);
347-
return constructor.newInstance(args);
347+
return BeanUtils.instantiateClass(constructor, args);
348348
}
349349

350350
private Object instantiate(ConfigurableBeanFactory beanFactory, Method method, Object[] args) throws Exception {

spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java

+40-6
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,23 @@
2424
import java.util.Arrays;
2525
import java.util.function.Consumer;
2626

27+
import kotlin.jvm.JvmClassMappingKt;
28+
import kotlin.reflect.KClass;
29+
import kotlin.reflect.KFunction;
30+
import kotlin.reflect.KParameter;
31+
2732
import org.springframework.aot.generate.AccessControl;
2833
import org.springframework.aot.generate.AccessControl.Visibility;
2934
import org.springframework.aot.generate.GeneratedMethod;
3035
import org.springframework.aot.generate.GeneratedMethods;
3136
import org.springframework.aot.generate.GenerationContext;
3237
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
3338
import org.springframework.aot.hint.ExecutableMode;
39+
import org.springframework.aot.hint.MemberCategory;
40+
import org.springframework.aot.hint.ReflectionHints;
3441
import org.springframework.beans.factory.support.InstanceSupplier;
3542
import org.springframework.beans.factory.support.RegisteredBean;
43+
import org.springframework.core.KotlinDetector;
3644
import org.springframework.core.ResolvableType;
3745
import org.springframework.javapoet.ClassName;
3846
import org.springframework.javapoet.CodeBlock;
@@ -56,6 +64,7 @@
5664
* @author Phillip Webb
5765
* @author Stephane Nicoll
5866
* @author Juergen Hoeller
67+
* @author Sebastien Deleuze
5968
* @since 6.0
6069
*/
6170
class InstanceSupplierCodeGenerator {
@@ -108,11 +117,16 @@ private CodeBlock generateCodeForConstructor(RegisteredBean registeredBean, Cons
108117
boolean dependsOnBean = ClassUtils.isInnerClass(declaringClass);
109118

110119
Visibility accessVisibility = getAccessVisibility(registeredBean, constructor);
111-
if (accessVisibility != Visibility.PRIVATE) {
120+
if (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasConstructorWithOptionalParameter(beanClass)) {
121+
return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor,
122+
dependsOnBean, hints -> hints.registerType(beanClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
123+
}
124+
else if (accessVisibility != Visibility.PRIVATE) {
112125
return generateCodeForAccessibleConstructor(beanName, beanClass, constructor,
113126
dependsOnBean, declaringClass);
114127
}
115-
return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, dependsOnBean);
128+
return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, dependsOnBean,
129+
hints -> hints.registerConstructor(constructor, ExecutableMode.INVOKE));
116130
}
117131

118132
private CodeBlock generateCodeForAccessibleConstructor(String beanName, Class<?> beanClass,
@@ -137,11 +151,10 @@ private CodeBlock generateCodeForAccessibleConstructor(String beanName, Class<?>
137151
return generateReturnStatement(generatedMethod);
138152
}
139153

140-
private CodeBlock generateCodeForInaccessibleConstructor(String beanName,
141-
Class<?> beanClass, Constructor<?> constructor, boolean dependsOnBean) {
154+
private CodeBlock generateCodeForInaccessibleConstructor(String beanName, Class<?> beanClass,
155+
Constructor<?> constructor, boolean dependsOnBean, Consumer<ReflectionHints> hints) {
142156

143-
this.generationContext.getRuntimeHints().reflection()
144-
.registerConstructor(constructor, ExecutableMode.INVOKE);
157+
hints.accept(this.generationContext.getRuntimeHints().reflection());
145158

146159
GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method -> {
147160
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
@@ -337,4 +350,25 @@ private boolean isThrowingCheckedException(Executable executable) {
337350
.anyMatch(Exception.class::isAssignableFrom);
338351
}
339352

353+
/**
354+
* Inner class to avoid a hard dependency on Kotlin at runtime.
355+
*/
356+
private static class KotlinDelegate {
357+
358+
public static boolean hasConstructorWithOptionalParameter(Class<?> beanClass) {
359+
if (KotlinDetector.isKotlinType(beanClass)) {
360+
KClass<?> kClass = JvmClassMappingKt.getKotlinClass(beanClass);
361+
for (KFunction<?> constructor : kClass.getConstructors()) {
362+
for (KParameter parameter : constructor.getParameters()) {
363+
if (parameter.isOptional()) {
364+
return true;
365+
}
366+
}
367+
}
368+
}
369+
return false;
370+
}
371+
372+
}
373+
340374
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2002-2023 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+
* https://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.factory.aot
18+
19+
import org.assertj.core.api.Assertions
20+
import org.assertj.core.api.ThrowingConsumer
21+
import org.junit.jupiter.api.Test
22+
import org.springframework.aot.hint.*
23+
import org.springframework.aot.test.generate.TestGenerationContext
24+
import org.springframework.beans.factory.config.BeanDefinition
25+
import org.springframework.beans.factory.support.DefaultListableBeanFactory
26+
import org.springframework.beans.factory.support.InstanceSupplier
27+
import org.springframework.beans.factory.support.RegisteredBean
28+
import org.springframework.beans.factory.support.RootBeanDefinition
29+
import org.springframework.beans.testfixture.beans.KotlinTestBean
30+
import org.springframework.beans.testfixture.beans.KotlinTestBeanWithOptionalParameter
31+
import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder
32+
import org.springframework.core.test.tools.Compiled
33+
import org.springframework.core.test.tools.TestCompiler
34+
import org.springframework.javapoet.MethodSpec
35+
import org.springframework.javapoet.ParameterizedTypeName
36+
import org.springframework.javapoet.TypeSpec
37+
import java.util.function.BiConsumer
38+
import java.util.function.Supplier
39+
import javax.lang.model.element.Modifier
40+
41+
/**
42+
* Kotlin tests for [InstanceSupplierCodeGenerator].
43+
*
44+
* @author Sebastien Deleuze
45+
*/
46+
class InstanceSupplierCodeGeneratorKotlinTests {
47+
48+
private val generationContext = TestGenerationContext()
49+
50+
@Test
51+
fun generateWhenHasDefaultConstructor() {
52+
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBean::class.java)
53+
val beanFactory = DefaultListableBeanFactory()
54+
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
55+
val bean = getBean<KotlinTestBean>(beanFactory, beanDefinition, instanceSupplier)
56+
Assertions.assertThat(bean).isInstanceOf(KotlinTestBean::class.java)
57+
Assertions.assertThat(compiled.sourceFile).contains("InstanceSupplier.using(KotlinTestBean::new)")
58+
}
59+
Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBean::class.java))
60+
.satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT))
61+
}
62+
63+
@Test
64+
fun generateWhenConstructorHasOptionalParameter() {
65+
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBeanWithOptionalParameter::class.java)
66+
val beanFactory = DefaultListableBeanFactory()
67+
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
68+
val bean: KotlinTestBeanWithOptionalParameter = getBean(beanFactory, beanDefinition, instanceSupplier)
69+
Assertions.assertThat(bean).isInstanceOf(KotlinTestBeanWithOptionalParameter::class.java)
70+
Assertions.assertThat(compiled.sourceFile)
71+
.contains("return BeanInstanceSupplier.<KotlinTestBeanWithOptionalParameter>forConstructor();")
72+
}
73+
Assertions.assertThat<TypeHint>(getReflectionHints().getTypeHint(KotlinTestBeanWithOptionalParameter::class.java))
74+
.satisfies(hasMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
75+
}
76+
77+
private fun getReflectionHints(): ReflectionHints {
78+
return generationContext.runtimeHints.reflection()
79+
}
80+
81+
private fun hasConstructorWithMode(mode: ExecutableMode): ThrowingConsumer<TypeHint> {
82+
return ThrowingConsumer {
83+
Assertions.assertThat(it.constructors()).anySatisfy(hasMode(mode))
84+
}
85+
}
86+
87+
private fun hasMemberCategory(category: MemberCategory): ThrowingConsumer<TypeHint> {
88+
return ThrowingConsumer {
89+
Assertions.assertThat(it.memberCategories).contains(category)
90+
}
91+
}
92+
93+
private fun hasMode(mode: ExecutableMode): ThrowingConsumer<ExecutableHint> {
94+
return ThrowingConsumer {
95+
Assertions.assertThat(it.mode).isEqualTo(mode)
96+
}
97+
}
98+
99+
@Suppress("UNCHECKED_CAST")
100+
private fun <T> getBean(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
101+
instanceSupplier: InstanceSupplier<*>): T {
102+
(beanDefinition as RootBeanDefinition).instanceSupplier = instanceSupplier
103+
beanFactory.registerBeanDefinition("testBean", beanDefinition)
104+
return beanFactory.getBean("testBean") as T
105+
}
106+
107+
private fun compile(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
108+
result: BiConsumer<InstanceSupplier<*>, Compiled>) {
109+
110+
val freshBeanFactory = DefaultListableBeanFactory(beanFactory)
111+
freshBeanFactory.registerBeanDefinition("testBean", beanDefinition)
112+
val registeredBean = RegisteredBean.of(freshBeanFactory, "testBean")
113+
val typeBuilder = DeferredTypeBuilder()
114+
val generateClass = generationContext.generatedClasses.addForFeature("TestCode", typeBuilder)
115+
val generator = InstanceSupplierCodeGenerator(
116+
generationContext, generateClass.name,
117+
generateClass.methods, false
118+
)
119+
val constructorOrFactoryMethod = registeredBean.resolveConstructorOrFactoryMethod()
120+
Assertions.assertThat(constructorOrFactoryMethod).isNotNull()
121+
val generatedCode = generator.generateCode(registeredBean, constructorOrFactoryMethod)
122+
typeBuilder.set { type: TypeSpec.Builder ->
123+
type.addModifiers(Modifier.PUBLIC)
124+
type.addSuperinterface(
125+
ParameterizedTypeName.get(
126+
Supplier::class.java,
127+
InstanceSupplier::class.java
128+
)
129+
)
130+
type.addMethod(
131+
MethodSpec.methodBuilder("get")
132+
.addModifiers(Modifier.PUBLIC)
133+
.returns(InstanceSupplier::class.java)
134+
.addStatement("return \$L", generatedCode).build()
135+
)
136+
}
137+
generationContext.writeGeneratedContent()
138+
TestCompiler.forSystem().with(generationContext).compile {
139+
result.accept(it.getInstance(Supplier::class.java).get() as InstanceSupplier<*>, it)
140+
}
141+
}
142+
143+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2002-2023 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+
* https://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.testfixture.beans
18+
19+
class KotlinTestBean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2002-2023 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+
* https://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.testfixture.beans
18+
19+
class KotlinTestBeanWithOptionalParameter(private val other: KotlinTestBean = KotlinTestBean())

0 commit comments

Comments
 (0)