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
This commit is contained in:
Sébastien Deleuze
2023-07-07 17:20:58 +02:00
parent 20dd66cd5a
commit a03a14797f
5 changed files with 223 additions and 8 deletions

View File

@@ -0,0 +1,143 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans.factory.aot
import org.assertj.core.api.Assertions
import org.assertj.core.api.ThrowingConsumer
import org.junit.jupiter.api.Test
import org.springframework.aot.hint.*
import org.springframework.aot.test.generate.TestGenerationContext
import org.springframework.beans.factory.config.BeanDefinition
import org.springframework.beans.factory.support.DefaultListableBeanFactory
import org.springframework.beans.factory.support.InstanceSupplier
import org.springframework.beans.factory.support.RegisteredBean
import org.springframework.beans.factory.support.RootBeanDefinition
import org.springframework.beans.testfixture.beans.KotlinTestBean
import org.springframework.beans.testfixture.beans.KotlinTestBeanWithOptionalParameter
import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder
import org.springframework.core.test.tools.Compiled
import org.springframework.core.test.tools.TestCompiler
import org.springframework.javapoet.MethodSpec
import org.springframework.javapoet.ParameterizedTypeName
import org.springframework.javapoet.TypeSpec
import java.util.function.BiConsumer
import java.util.function.Supplier
import javax.lang.model.element.Modifier
/**
* Kotlin tests for [InstanceSupplierCodeGenerator].
*
* @author Sebastien Deleuze
*/
class InstanceSupplierCodeGeneratorKotlinTests {
private val generationContext = TestGenerationContext()
@Test
fun generateWhenHasDefaultConstructor() {
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBean::class.java)
val beanFactory = DefaultListableBeanFactory()
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
val bean = getBean<KotlinTestBean>(beanFactory, beanDefinition, instanceSupplier)
Assertions.assertThat(bean).isInstanceOf(KotlinTestBean::class.java)
Assertions.assertThat(compiled.sourceFile).contains("InstanceSupplier.using(KotlinTestBean::new)")
}
Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBean::class.java))
.satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT))
}
@Test
fun generateWhenConstructorHasOptionalParameter() {
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBeanWithOptionalParameter::class.java)
val beanFactory = DefaultListableBeanFactory()
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
val bean: KotlinTestBeanWithOptionalParameter = getBean(beanFactory, beanDefinition, instanceSupplier)
Assertions.assertThat(bean).isInstanceOf(KotlinTestBeanWithOptionalParameter::class.java)
Assertions.assertThat(compiled.sourceFile)
.contains("return BeanInstanceSupplier.<KotlinTestBeanWithOptionalParameter>forConstructor();")
}
Assertions.assertThat<TypeHint>(getReflectionHints().getTypeHint(KotlinTestBeanWithOptionalParameter::class.java))
.satisfies(hasMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
}
private fun getReflectionHints(): ReflectionHints {
return generationContext.runtimeHints.reflection()
}
private fun hasConstructorWithMode(mode: ExecutableMode): ThrowingConsumer<TypeHint> {
return ThrowingConsumer {
Assertions.assertThat(it.constructors()).anySatisfy(hasMode(mode))
}
}
private fun hasMemberCategory(category: MemberCategory): ThrowingConsumer<TypeHint> {
return ThrowingConsumer {
Assertions.assertThat(it.memberCategories).contains(category)
}
}
private fun hasMode(mode: ExecutableMode): ThrowingConsumer<ExecutableHint> {
return ThrowingConsumer {
Assertions.assertThat(it.mode).isEqualTo(mode)
}
}
@Suppress("UNCHECKED_CAST")
private fun <T> getBean(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
instanceSupplier: InstanceSupplier<*>): T {
(beanDefinition as RootBeanDefinition).instanceSupplier = instanceSupplier
beanFactory.registerBeanDefinition("testBean", beanDefinition)
return beanFactory.getBean("testBean") as T
}
private fun compile(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
result: BiConsumer<InstanceSupplier<*>, Compiled>) {
val freshBeanFactory = DefaultListableBeanFactory(beanFactory)
freshBeanFactory.registerBeanDefinition("testBean", beanDefinition)
val registeredBean = RegisteredBean.of(freshBeanFactory, "testBean")
val typeBuilder = DeferredTypeBuilder()
val generateClass = generationContext.generatedClasses.addForFeature("TestCode", typeBuilder)
val generator = InstanceSupplierCodeGenerator(
generationContext, generateClass.name,
generateClass.methods, false
)
val constructorOrFactoryMethod = registeredBean.resolveConstructorOrFactoryMethod()
Assertions.assertThat(constructorOrFactoryMethod).isNotNull()
val generatedCode = generator.generateCode(registeredBean, constructorOrFactoryMethod)
typeBuilder.set { type: TypeSpec.Builder ->
type.addModifiers(Modifier.PUBLIC)
type.addSuperinterface(
ParameterizedTypeName.get(
Supplier::class.java,
InstanceSupplier::class.java
)
)
type.addMethod(
MethodSpec.methodBuilder("get")
.addModifiers(Modifier.PUBLIC)
.returns(InstanceSupplier::class.java)
.addStatement("return \$L", generatedCode).build()
)
}
generationContext.writeGeneratedContent()
TestCompiler.forSystem().with(generationContext).compile {
result.accept(it.getInstance(Supplier::class.java).get() as InstanceSupplier<*>, it)
}
}
}