diff --git a/spring-context/src/main/java/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessor.java new file mode 100644 index 000000000000..ca616b8e5744 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessor.java @@ -0,0 +1,72 @@ +/* + * 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.context.aot; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.KotlinDetector; +import org.springframework.lang.Nullable; + +/** + * AOT {@code BeanRegistrationAotProcessor} that adds additional hints + * required by Kotlin reflection. + * + * @author Sebastien Deleuze + * @since 6.0.4 + */ +class KotlinReflectionBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { + + @Nullable + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + Class beanClass = registeredBean.getBeanClass(); + if (KotlinDetector.isKotlinType(beanClass)) { + return new KotlinReflectionBeanRegistrationAotContribution(beanClass); + } + return null; + } + + private static class KotlinReflectionBeanRegistrationAotContribution implements BeanRegistrationAotContribution { + + private final Class beanClass; + + public KotlinReflectionBeanRegistrationAotContribution(Class beanClass) { + this.beanClass = beanClass; + } + + @Override + public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { + registerHints(this.beanClass, generationContext.getRuntimeHints()); + } + + private void registerHints(Class type, RuntimeHints runtimeHints) { + if (KotlinDetector.isKotlinType(type)) { + runtimeHints.reflection().registerType(type, MemberCategory.INTROSPECT_DECLARED_METHODS); + } + Class superClass = type.getSuperclass(); + if (superClass != null) { + registerHints(superClass, runtimeHints); + } + } + } + +} diff --git a/spring-context/src/main/resources/META-INF/spring/aot.factories b/spring-context/src/main/resources/META-INF/spring/aot.factories index 7ead6d3fb12c..f7518311cfdf 100644 --- a/spring-context/src/main/resources/META-INF/spring/aot.factories +++ b/spring-context/src/main/resources/META-INF/spring/aot.factories @@ -1,3 +1,5 @@ org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor= \ org.springframework.context.aot.ReflectiveProcessorBeanFactoryInitializationAotProcessor +org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ +org.springframework.context.aot.KotlinReflectionBeanRegistrationAotProcessor diff --git a/spring-context/src/test/java/org/springframework/context/aot/SampleJavaBean.java b/spring-context/src/test/java/org/springframework/context/aot/SampleJavaBean.java new file mode 100644 index 000000000000..313125ad4ef7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/aot/SampleJavaBean.java @@ -0,0 +1,23 @@ +/* + * 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.context.aot; + +class SampleJavaBean { + + void sample() { + } +} diff --git a/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessorTests.kt b/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessorTests.kt new file mode 100644 index 000000000000..ce3988c61410 --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessorTests.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2022 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.context.aot + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.springframework.aot.generate.GenerationContext +import org.springframework.aot.hint.MemberCategory +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates +import org.springframework.aot.test.generate.TestGenerationContext +import org.springframework.beans.factory.aot.* +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.beans.factory.support.RegisteredBean +import org.springframework.beans.factory.support.RootBeanDefinition + +/** + * Tests for [KotlinReflectionBeanRegistrationAotProcessor]. + * + * @author Sebastien Deleuze + */ +class KotlinReflectionBeanRegistrationAotProcessorTests { + + private val processor = KotlinReflectionBeanRegistrationAotProcessor() + + private val generationContext = TestGenerationContext() + + @Test + fun processorIsRegistered() { + Assertions.assertThat( + AotServices.factories(javaClass.classLoader).load(BeanRegistrationAotProcessor::class.java)) + .anyMatch(KotlinReflectionBeanRegistrationAotProcessor::class.java::isInstance) + } + + @Test + fun shouldProcessKotlinBean() { + process(SampleKotlinBean::class.java) + Assertions.assertThat( + RuntimeHintsPredicates.reflection() + .onType(SampleKotlinBean::class.java) + .withMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS) + ).accepts(generationContext.runtimeHints) + Assertions.assertThat( + RuntimeHintsPredicates.reflection() + .onType(BaseKotlinBean::class.java) + .withMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS) + ).accepts(generationContext.runtimeHints) + } + + @Test + fun shouldNotProcessJavaBean() { + process(SampleJavaBean::class.java) + Assertions.assertThat(generationContext.runtimeHints.reflection().typeHints()).isEmpty() + } + + private fun process(beanClass: Class<*>) { + createContribution(beanClass)?.applyTo(generationContext, Mockito.mock(BeanRegistrationCode::class.java)) + } + + private fun createContribution(beanClass: Class<*>): BeanRegistrationAotContribution? { + val beanFactory = DefaultListableBeanFactory() + beanFactory.registerBeanDefinition(beanClass.name, RootBeanDefinition(beanClass)) + return processor.processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.name)) + } + + + class SampleKotlinBean : BaseKotlinBean() { + fun sample() { + } + } + + open class BaseKotlinBean { + fun base() { + } + } + +}