diff --git a/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java new file mode 100644 index 0000000000..4c7528e681 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java @@ -0,0 +1,61 @@ +/* + * 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.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.JavaFile; + +/** + * Process an {@link ApplicationContext} and its {@link BeanFactory} to generate + * code that represents the state of the bean factory, as well as the necessary + * hints that can be used at runtime in a constrained environment. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 6.0 + */ +public class ApplicationContextAotGenerator { + + /** + * Refresh the specified {@link GenericApplicationContext} and generate the + * necessary code to restore the state of its {@link BeanFactory}, using the + * specified {@link GenerationContext}. + * @param applicationContext the application context to handle + * @param generationContext the generation context to use + * @param generatedInitializerClassName the class name to use for the + * generated application context initializer + */ + public void generateApplicationContext(GenericApplicationContext applicationContext, + GenerationContext generationContext, + ClassName generatedInitializerClassName) { + + applicationContext.refreshForAotProcessing(); + DefaultListableBeanFactory beanFactory = applicationContext + .getDefaultListableBeanFactory(); + ApplicationContextInitializationCodeGenerator codeGenerator = new ApplicationContextInitializationCodeGenerator(); + new BeanFactoryInitializationContributions(beanFactory).applyTo(generationContext, + codeGenerator); + JavaFile javaFile = codeGenerator.generateJavaFile(generatedInitializerClassName); + generationContext.getGeneratedFiles().addSourceFile(javaFile); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.java b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.java new file mode 100644 index 0000000000..411a58cb11 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.java @@ -0,0 +1,103 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +import javax.lang.model.element.Modifier; + +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.MethodGenerator; +import org.springframework.aot.generate.MethodReference; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.JavaFile; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; +import org.springframework.javapoet.TypeSpec; + +/** + * Internal code generator to create the application context initializer. + * + * @author Phillip Webb + * @since 6.0 + */ +class ApplicationContextInitializationCodeGenerator + implements BeanFactoryInitializationCode { + + private static final String APPLICATION_CONTEXT_VARIABLE = "applicationContext"; + + + private final GeneratedMethods generatedMethods = new GeneratedMethods(); + + private final List initializers = new ArrayList<>(); + + + @Override + public MethodGenerator getMethodGenerator() { + return this.generatedMethods; + } + + @Override + public void addInitializer(MethodReference methodReference) { + this.initializers.add(methodReference); + } + + JavaFile generateJavaFile(ClassName className) { + TypeSpec.Builder builder = TypeSpec.classBuilder(className); + builder.addJavadoc( + "{@link $T} to restore an application context based on previous AOT processing.", + ApplicationContextInitializer.class); + builder.addModifiers(Modifier.PUBLIC); + builder.addSuperinterface(ParameterizedTypeName.get( + ApplicationContextInitializer.class, GenericApplicationContext.class)); + builder.addMethod(generateInitializeMethod()); + this.generatedMethods.doWithMethodSpecs(builder::addMethod); + return JavaFile.builder(className.packageName(), builder.build()).build(); + } + + private MethodSpec generateInitializeMethod() { + MethodSpec.Builder builder = MethodSpec.methodBuilder("initialize"); + builder.addAnnotation(Override.class); + builder.addModifiers(Modifier.PUBLIC); + builder.addParameter(GenericApplicationContext.class, + APPLICATION_CONTEXT_VARIABLE); + builder.addCode(generateInitializeCode()); + return builder.build(); + } + + private CodeBlock generateInitializeCode() { + CodeBlock.Builder builder = CodeBlock.builder(); + builder.addStatement("$T $L = $L.getDefaultListableBeanFactory()", + DefaultListableBeanFactory.class, BEAN_FACTORY_VARIABLE, + APPLICATION_CONTEXT_VARIABLE); + builder.addStatement("$L.setAutowireCandidateResolver(new $T())", + BEAN_FACTORY_VARIABLE, ContextAnnotationAutowireCandidateResolver.class); + for (MethodReference initializer : this.initializers) { + builder.addStatement( + initializer.toInvokeCodeBlock(CodeBlock.of(BEAN_FACTORY_VARIABLE))); + } + return builder.build(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/aot/BeanFactoryInitializationContributions.java b/spring-context/src/main/java/org/springframework/context/aot/BeanFactoryInitializationContributions.java new file mode 100644 index 0000000000..bf03a9d589 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/aot/BeanFactoryInitializationContributions.java @@ -0,0 +1,82 @@ +/* + * 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.aot.AotFactoriesLoader; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; + +/** + * A collection of {@link BeanFactoryInitializationAotContribution AOT + * contributions} obtained from {@link BeanFactoryInitializationAotProcessor AOT + * processors}. + * + * @author Phillip Webb + * @since 6.0 + */ +class BeanFactoryInitializationContributions { + + private final List contributions; + + + BeanFactoryInitializationContributions(DefaultListableBeanFactory beanFactory) { + this(beanFactory, new AotFactoriesLoader(beanFactory)); + } + + BeanFactoryInitializationContributions(DefaultListableBeanFactory beanFactory, + AotFactoriesLoader loader) { + this.contributions = getContributions(beanFactory, getProcessors(loader)); + } + + + private List getProcessors( + AotFactoriesLoader loader) { + List processors = new ArrayList<>( + loader.load(BeanFactoryInitializationAotProcessor.class)); + processors.add(new RuntimeHintsBeanFactoryInitializationAotProcessor()); + return Collections.unmodifiableList(processors); + } + + private List getContributions( + DefaultListableBeanFactory beanFactory, + List processors) { + List contributions = new ArrayList<>(); + for (BeanFactoryInitializationAotProcessor processor : processors) { + BeanFactoryInitializationAotContribution contribution = processor + .processAheadOfTime(beanFactory); + if (contribution != null) { + contributions.add(contribution); + } + } + return Collections.unmodifiableList(contributions); + } + + void applyTo(GenerationContext generationContext, + BeanFactoryInitializationCode beanFactoryInitializationCode) { + for (BeanFactoryInitializationAotContribution contribution : this.contributions) { + contribution.applyTo(generationContext, beanFactoryInitializationCode); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessor.java new file mode 100644 index 0000000000..1b0039f28b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessor.java @@ -0,0 +1,122 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.aot.AotFactoriesLoader; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.core.log.LogMessage; +import org.springframework.lang.Nullable; + +/** + * AOT {@code BeanFactoryPostProcessor} that processes + * {@link RuntimeHintsRegistrar} implementations declared as + * {@code spring.factories} or using + * {@link ImportRuntimeHints @ImportRuntimeHints} annotated configuration + * classes or bean methods. + *

+ * This processor is registered by default in the + * {@link ApplicationContextAotGenerator} as it is only useful in an AOT + * context. + * + * @author Brian Clozel + * @see ApplicationContextAotGenerator + */ +class RuntimeHintsBeanFactoryInitializationAotProcessor + implements BeanFactoryInitializationAotProcessor { + + private static final Log logger = LogFactory + .getLog(RuntimeHintsBeanFactoryInitializationAotProcessor.class); + + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime( + ConfigurableListableBeanFactory beanFactory) { + AotFactoriesLoader loader = new AotFactoriesLoader(beanFactory); + List registrars = new ArrayList<>( + loader.load(RuntimeHintsRegistrar.class)); + for (String beanName : beanFactory + .getBeanNamesForAnnotation(ImportRuntimeHints.class)) { + ImportRuntimeHints annotation = beanFactory.findAnnotationOnBean(beanName, + ImportRuntimeHints.class); + if (annotation != null) { + registrars.addAll(extracted(beanName, annotation)); + } + } + return new RuntimeHintsRegistrarContribution(registrars, + beanFactory.getBeanClassLoader()); + } + + private List extracted(String beanName, + ImportRuntimeHints annotation) { + Class[] registrarClasses = annotation.value(); + List registrars = new ArrayList<>(registrarClasses.length); + for (Class registrarClass : registrarClasses) { + logger.trace( + LogMessage.format("Loaded [%s] registrar from annotated bean [%s]", + registrarClass.getCanonicalName(), beanName)); + registrars.add(BeanUtils.instantiateClass(registrarClass)); + } + return registrars; + } + + + static class RuntimeHintsRegistrarContribution + implements BeanFactoryInitializationAotContribution { + + + private final List registrars; + + @Nullable + private final ClassLoader beanClassLoader; + + + RuntimeHintsRegistrarContribution(List registrars, + @Nullable ClassLoader beanClassLoader) { + this.registrars = registrars; + this.beanClassLoader = beanClassLoader; + } + + + @Override + public void applyTo(GenerationContext generationContext, + BeanFactoryInitializationCode beanFactoryInitializationCode) { + RuntimeHints hints = generationContext.getRuntimeHints(); + this.registrars.forEach(registrar -> { + logger.trace(LogMessage.format( + "Processing RuntimeHints contribution from [%s]", + registrar.getClass().getCanonicalName())); + registrar.registerHints(hints, this.beanClassLoader); + }); + } + + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/aot/package-info.java b/spring-context/src/main/java/org/springframework/context/aot/package-info.java new file mode 100644 index 0000000000..abb1763028 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/aot/package-info.java @@ -0,0 +1,9 @@ +/** + * AOT support for application contexts. + */ +@NonNullApi +@NonNullFields +package org.springframework.context.aot; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java new file mode 100644 index 0000000000..f3a5baf964 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java @@ -0,0 +1,242 @@ +/* + * 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 java.util.function.BiConsumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.DefaultGenerationContext; +import org.springframework.aot.generate.InMemoryGeneratedFiles; +import org.springframework.aot.test.generator.compile.Compiled; +import org.springframework.aot.test.generator.compile.TestCompiler; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.context.testfixture.context.generator.SimpleComponent; +import org.springframework.context.testfixture.context.generator.annotation.AutowiredComponent; +import org.springframework.context.testfixture.context.generator.annotation.InitDestroyComponent; +import org.springframework.javapoet.ClassName; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ApplicationContextAotGenerator}. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +class ApplicationContextAotGeneratorTests { + + private static final ClassName MAIN_GENERATED_TYPE = ClassName.get("__", + "TestInitializer"); + + @Test + void generateApplicationContextWhenHasSimpleBean() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.registerBeanDefinition("test", + new RootBeanDefinition(SimpleComponent.class)); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext( + initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()) + .containsOnly("test"); + assertThat(freshApplicationContext.getBean("test")) + .isInstanceOf(SimpleComponent.class); + }); + } + + @Test + void generateApplicationContextWhenHasAutowiring() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.registerBeanDefinition( + AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME, + BeanDefinitionBuilder + .rootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE).getBeanDefinition()); + applicationContext.registerBeanDefinition("autowiredComponent", + new RootBeanDefinition(AutowiredComponent.class)); + applicationContext.registerBeanDefinition("number", + BeanDefinitionBuilder.rootBeanDefinition(Integer.class, "valueOf") + .addConstructorArgValue("42").getBeanDefinition()); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext( + initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()) + .containsOnly("autowiredComponent", "number"); + AutowiredComponent bean = freshApplicationContext + .getBean(AutowiredComponent.class); + assertThat(bean.getEnvironment()) + .isSameAs(freshApplicationContext.getEnvironment()); + assertThat(bean.getCounter()).isEqualTo(42); + }); + } + + @Test + void generateApplicationContextWhenHasInitDestroyMethods() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.registerBeanDefinition( + AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME, + BeanDefinitionBuilder + .rootBeanDefinition(CommonAnnotationBeanPostProcessor.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE).getBeanDefinition()); + applicationContext.registerBeanDefinition("initDestroyComponent", + new RootBeanDefinition(InitDestroyComponent.class)); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext( + initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()) + .containsOnly("initDestroyComponent"); + InitDestroyComponent bean = freshApplicationContext + .getBean(InitDestroyComponent.class); + assertThat(bean.events).containsExactly("init"); + freshApplicationContext.close(); + assertThat(bean.events).containsExactly("init", "destroy"); + }); + } + + @Test + void generateApplicationContextWhenHasMultipleInitDestroyMethods() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.registerBeanDefinition( + AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME, + BeanDefinitionBuilder + .rootBeanDefinition(CommonAnnotationBeanPostProcessor.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE).getBeanDefinition()); + RootBeanDefinition beanDefinition = new RootBeanDefinition( + InitDestroyComponent.class); + beanDefinition.setInitMethodName("customInit"); + beanDefinition.setDestroyMethodName("customDestroy"); + applicationContext.registerBeanDefinition("initDestroyComponent", beanDefinition); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext( + initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()) + .containsOnly("initDestroyComponent"); + InitDestroyComponent bean = freshApplicationContext + .getBean(InitDestroyComponent.class); + assertThat(bean.events).containsExactly("customInit", "init"); + freshApplicationContext.close(); + assertThat(bean.events).containsExactly("customInit", "init", "customDestroy", + "destroy"); + }); + } + + @Test + void generateApplicationContextWhenHasNoAotContributions() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext( + initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()).isEmpty(); + assertThat(compiled.getSourceFile()).contains( + "beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver())"); + }); + } + + @Test + void generateApplicationContextWhenHasBeanFactoryInitializationAotProcessorExcludesProcessor() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.registerBeanDefinition("test", + new RootBeanDefinition(NoOpBeanFactoryInitializationAotProcessor.class)); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext( + initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()).isEmpty(); + }); + } + + @Test + void generateApplicationContextWhenHasBeanRegistrationAotProcessorExcludesProcessor() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.registerBeanDefinition("test", + new RootBeanDefinition(NoOpBeanRegistrationAotProcessor.class)); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext( + initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()).isEmpty(); + }); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void testCompiledResult(GenericApplicationContext applicationContext, + BiConsumer, Compiled> result) { + ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); + InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles(); + DefaultGenerationContext generationContext = new DefaultGenerationContext( + generatedFiles); + generator.generateApplicationContext(applicationContext, generationContext, + MAIN_GENERATED_TYPE); + generationContext.writeGeneratedContent(); + TestCompiler.forSystem().withFiles(generatedFiles) + .compile(compiled -> result.accept( + compiled.getInstance(ApplicationContextInitializer.class), + compiled)); + } + + private GenericApplicationContext toFreshApplicationContext( + ApplicationContextInitializer initializer) { + GenericApplicationContext freshApplicationContext = new GenericApplicationContext(); + initializer.initialize(freshApplicationContext); + freshApplicationContext.refresh(); + return freshApplicationContext; + } + + + static class NoOpBeanFactoryInitializationAotProcessor + implements BeanFactoryPostProcessor, BeanFactoryInitializationAotProcessor { + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + } + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime( + ConfigurableListableBeanFactory beanFactory) { + return null; + } + + } + + + static class NoOpBeanRegistrationAotProcessor + implements BeanPostProcessor, BeanRegistrationAotProcessor { + + @Override + public BeanRegistrationAotContribution processAheadOfTime( + RegisteredBean registeredBean) { + return null; + } + + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java new file mode 100644 index 0000000000..2bd2f3f7eb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java @@ -0,0 +1,197 @@ +/* + * 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 java.io.IOException; +import java.net.URL; +import java.util.Enumeration; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.DefaultGenerationContext; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.InMemoryGeneratedFiles; +import org.springframework.aot.hint.ResourceBundleHint; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.javapoet.ClassName; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link RuntimeHintsBeanFactoryInitializationAotProcessor}. + * + * @author Brian Clozel + */ +class RuntimeHintsBeanFactoryInitializationAotProcessorTests { + + private static final ClassName MAIN_GENERATED_TYPE = ClassName.get("__", + "TestInitializer"); + + private GenerationContext generationContext; + + private ApplicationContextAotGenerator generator; + + @BeforeEach + void setup() { + this.generationContext = new DefaultGenerationContext( + new InMemoryGeneratedFiles()); + this.generator = new ApplicationContextAotGenerator(); + } + + @Test + void shouldProcessRegistrarOnConfiguration() { + GenericApplicationContext applicationContext = createApplicationContext( + ConfigurationWithHints.class); + this.generator.generateApplicationContext(applicationContext, + this.generationContext, MAIN_GENERATED_TYPE); + assertThatSampleRegistrarContributed(); + } + + @Test + void shouldProcessRegistrarOnBeanMethod() { + GenericApplicationContext applicationContext = createApplicationContext( + ConfigurationWithBeanDeclaringHints.class); + this.generator.generateApplicationContext(applicationContext, + this.generationContext, MAIN_GENERATED_TYPE); + assertThatSampleRegistrarContributed(); + } + + @Test + void shouldProcessRegistrarInSpringFactory() { + GenericApplicationContext applicationContext = createApplicationContext(); + applicationContext.setClassLoader( + new TestSpringFactoriesClassLoader("test-runtime-hints-aot.factories")); + this.generator.generateApplicationContext(applicationContext, + this.generationContext, MAIN_GENERATED_TYPE); + assertThatSampleRegistrarContributed(); + } + + @Test + void shouldRejectRuntimeHintsRegistrarWithoutDefaultConstructor() { + GenericApplicationContext applicationContext = createApplicationContext( + ConfigurationWithIllegalRegistrar.class); + assertThatThrownBy(() -> this.generator.generateApplicationContext( + applicationContext, this.generationContext, MAIN_GENERATED_TYPE)) + .isInstanceOf(BeanInstantiationException.class); + } + + private void assertThatSampleRegistrarContributed() { + Stream bundleHints = this.generationContext.getRuntimeHints() + .resources().resourceBundles(); + assertThat(bundleHints) + .anyMatch(bundleHint -> "sample".equals(bundleHint.getBaseName())); + } + + private GenericApplicationContext createApplicationContext( + Class... configClasses) { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + AnnotationConfigUtils.registerAnnotationConfigProcessors(applicationContext); + for (Class configClass : configClasses) { + applicationContext.registerBeanDefinition(configClass.getSimpleName(), + new RootBeanDefinition(configClass)); + } + return applicationContext; + } + + + @ImportRuntimeHints(SampleRuntimeHintsRegistrar.class) + @Configuration(proxyBeanMethods = false) + static class ConfigurationWithHints { + + } + + + @Configuration(proxyBeanMethods = false) + static class ConfigurationWithBeanDeclaringHints { + + @Bean + @ImportRuntimeHints(SampleRuntimeHintsRegistrar.class) + SampleBean sampleBean() { + return new SampleBean(); + } + + } + + + public static class SampleRuntimeHintsRegistrar implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints.resources().registerResourceBundle("sample"); + } + + } + + + static class SampleBean { + + } + + + @ImportRuntimeHints(IllegalRuntimeHintsRegistrar.class) + @Configuration(proxyBeanMethods = false) + static class ConfigurationWithIllegalRegistrar { + + } + + + public static class IllegalRuntimeHintsRegistrar implements RuntimeHintsRegistrar { + + public IllegalRuntimeHintsRegistrar(String arg) { + + } + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints.resources().registerResourceBundle("sample"); + } + + } + + + static class TestSpringFactoriesClassLoader extends ClassLoader { + + private final String factoriesName; + + TestSpringFactoriesClassLoader(String factoriesName) { + super(Thread.currentThread().getContextClassLoader()); + this.factoriesName = factoriesName; + } + + @Override + public Enumeration getResources(String name) throws IOException { + if ("META-INF/spring/aot.factories".equals(name)) { + return super.getResources( + "org/springframework/context/aot/" + this.factoriesName); + } + return super.getResources(name); + } + + } + +} diff --git a/spring-context/src/test/resources/org/springframework/context/aot/test-runtime-hints-aot.factories b/spring-context/src/test/resources/org/springframework/context/aot/test-runtime-hints-aot.factories new file mode 100644 index 0000000000..6f94667355 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/aot/test-runtime-hints-aot.factories @@ -0,0 +1,2 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar= \ +org.springframework.context.aot.RuntimeHintsBeanFactoryInitializationAotProcessorTests.SampleRuntimeHintsRegistrar \ No newline at end of file