diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java index 020164115da..2480853257b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java @@ -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 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 { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java index 171a9206128..05f64bc6e6a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java @@ -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 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; + } + + } + } diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt new file mode 100644 index 00000000000..6623bb4581f --- /dev/null +++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt @@ -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(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.forConstructor();") + } + Assertions.assertThat(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 { + return ThrowingConsumer { + Assertions.assertThat(it.constructors()).anySatisfy(hasMode(mode)) + } + } + + private fun hasMemberCategory(category: MemberCategory): ThrowingConsumer { + return ThrowingConsumer { + Assertions.assertThat(it.memberCategories).contains(category) + } + } + + private fun hasMode(mode: ExecutableMode): ThrowingConsumer { + return ThrowingConsumer { + Assertions.assertThat(it.mode).isEqualTo(mode) + } + } + + @Suppress("UNCHECKED_CAST") + private fun 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, 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) + } + } + +} diff --git a/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinTestBean.kt b/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinTestBean.kt new file mode 100644 index 00000000000..1cdcc078b3d --- /dev/null +++ b/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinTestBean.kt @@ -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 diff --git a/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinTestBeanWithOptionalParameter.kt b/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinTestBeanWithOptionalParameter.kt new file mode 100644 index 00000000000..60ba9999be5 --- /dev/null +++ b/spring-beans/src/testFixtures/kotlin/org/springframework/beans/testfixture/beans/KotlinTestBeanWithOptionalParameter.kt @@ -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())