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:
parent
20dd66cd5a
commit
a03a14797f
|
|
@ -27,6 +27,7 @@ import java.util.stream.Collectors;
|
|||
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.beans.BeanInstantiationException;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.TypeConverter;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
|
|
@ -343,8 +344,7 @@ public final class BeanInstanceSupplier<T> extends AutowiredElementResolver impl
|
|||
Object enclosingInstance = createInstance(declaringClass.getEnclosingClass());
|
||||
args = ObjectUtils.addObjectToArray(args, enclosingInstance, 0);
|
||||
}
|
||||
ReflectionUtils.makeAccessible(constructor);
|
||||
return constructor.newInstance(args);
|
||||
return BeanUtils.instantiateClass(constructor, args);
|
||||
}
|
||||
|
||||
private Object instantiate(ConfigurableBeanFactory beanFactory, Method method, Object[] args) throws Exception {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@ import java.lang.reflect.Modifier;
|
|||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import kotlin.jvm.JvmClassMappingKt;
|
||||
import kotlin.reflect.KClass;
|
||||
import kotlin.reflect.KFunction;
|
||||
import kotlin.reflect.KParameter;
|
||||
|
||||
import org.springframework.aot.generate.AccessControl;
|
||||
import org.springframework.aot.generate.AccessControl.Visibility;
|
||||
import org.springframework.aot.generate.GeneratedMethod;
|
||||
|
|
@ -31,8 +36,11 @@ import org.springframework.aot.generate.GeneratedMethods;
|
|||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.beans.factory.support.InstanceSupplier;
|
||||
import org.springframework.beans.factory.support.RegisteredBean;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.javapoet.ClassName;
|
||||
import org.springframework.javapoet.CodeBlock;
|
||||
|
|
@ -56,6 +64,7 @@ import org.springframework.util.function.ThrowingSupplier;
|
|||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
* @author Juergen Hoeller
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.0
|
||||
*/
|
||||
class InstanceSupplierCodeGenerator {
|
||||
|
|
@ -108,11 +117,16 @@ class InstanceSupplierCodeGenerator {
|
|||
boolean dependsOnBean = ClassUtils.isInnerClass(declaringClass);
|
||||
|
||||
Visibility accessVisibility = getAccessVisibility(registeredBean, constructor);
|
||||
if (accessVisibility != Visibility.PRIVATE) {
|
||||
if (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasConstructorWithOptionalParameter(beanClass)) {
|
||||
return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor,
|
||||
dependsOnBean, hints -> hints.registerType(beanClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
|
||||
}
|
||||
else if (accessVisibility != Visibility.PRIVATE) {
|
||||
return generateCodeForAccessibleConstructor(beanName, beanClass, constructor,
|
||||
dependsOnBean, declaringClass);
|
||||
}
|
||||
return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, dependsOnBean);
|
||||
return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, dependsOnBean,
|
||||
hints -> hints.registerConstructor(constructor, ExecutableMode.INVOKE));
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForAccessibleConstructor(String beanName, Class<?> beanClass,
|
||||
|
|
@ -137,11 +151,10 @@ class InstanceSupplierCodeGenerator {
|
|||
return generateReturnStatement(generatedMethod);
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForInaccessibleConstructor(String beanName,
|
||||
Class<?> beanClass, Constructor<?> constructor, boolean dependsOnBean) {
|
||||
private CodeBlock generateCodeForInaccessibleConstructor(String beanName, Class<?> beanClass,
|
||||
Constructor<?> constructor, boolean dependsOnBean, Consumer<ReflectionHints> hints) {
|
||||
|
||||
this.generationContext.getRuntimeHints().reflection()
|
||||
.registerConstructor(constructor, ExecutableMode.INVOKE);
|
||||
hints.accept(this.generationContext.getRuntimeHints().reflection());
|
||||
|
||||
GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method -> {
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
|
||||
|
|
@ -337,4 +350,25 @@ class InstanceSupplierCodeGenerator {
|
|||
.anyMatch(Exception.class::isAssignableFrom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner class to avoid a hard dependency on Kotlin at runtime.
|
||||
*/
|
||||
private static class KotlinDelegate {
|
||||
|
||||
public static boolean hasConstructorWithOptionalParameter(Class<?> beanClass) {
|
||||
if (KotlinDetector.isKotlinType(beanClass)) {
|
||||
KClass<?> kClass = JvmClassMappingKt.getKotlinClass(beanClass);
|
||||
for (KFunction<?> constructor : kClass.getConstructors()) {
|
||||
for (KParameter parameter : constructor.getParameters()) {
|
||||
if (parameter.isOptional()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.testfixture.beans
|
||||
|
||||
class KotlinTestBean
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.testfixture.beans
|
||||
|
||||
class KotlinTestBeanWithOptionalParameter(private val other: KotlinTestBean = KotlinTestBean())
|
||||
Loading…
Reference in New Issue