diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java new file mode 100644 index 00000000000..5950c7db5a5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java @@ -0,0 +1,48 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.aot.hint.RuntimeHintsRegistrar; + +/** + * Indicates that one or more {@link RuntimeHintsRegistrar} implementations should be processed. + *

Unlike declaring {@link RuntimeHintsRegistrar} as {@code spring.factories}, + * {@code @ImportRuntimeHints} allows for more flexible use cases where registrations are only + * processed if the annotated configuration class or bean method is considered by the + * application context. + * + * @author Brian Clozel + * @since 6.0 + * @see org.springframework.aot.hint.RuntimeHints + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ImportRuntimeHints { + + /** + * {@link RuntimeHintsRegistrar} implementations to process. + */ + Class[] value(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/generator/ApplicationContextAotGenerator.java b/spring-context/src/main/java/org/springframework/context/generator/ApplicationContextAotGenerator.java index 5e800e0a8ec..5159e21cb62 100644 --- a/spring-context/src/main/java/org/springframework/context/generator/ApplicationContextAotGenerator.java +++ b/spring-context/src/main/java/org/springframework/context/generator/ApplicationContextAotGenerator.java @@ -150,6 +150,7 @@ public class ApplicationContextAotGenerator { for (String ppName : postProcessorNames) { postProcessors.add(beanFactory.getBean(ppName, AotContributingBeanFactoryPostProcessor.class)); } + postProcessors.add(new RuntimeHintsPostProcessor()); sortPostProcessors(postProcessors, beanFactory); return postProcessors; } diff --git a/spring-context/src/main/java/org/springframework/context/generator/RuntimeHintsPostProcessor.java b/spring-context/src/main/java/org/springframework/context/generator/RuntimeHintsPostProcessor.java new file mode 100644 index 00000000000..c28dea2b0f8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/generator/RuntimeHintsPostProcessor.java @@ -0,0 +1,92 @@ +/* + * 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.generator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.generator.AotContributingBeanFactoryPostProcessor; +import org.springframework.beans.factory.generator.BeanFactoryContribution; +import org.springframework.beans.factory.generator.BeanFactoryInitialization; +import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.core.io.support.SpringFactoriesLoader; +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 RuntimeHintsPostProcessor implements AotContributingBeanFactoryPostProcessor { + + private static final Log logger = LogFactory.getLog(RuntimeHintsPostProcessor.class); + + @Override + public BeanFactoryContribution contribute(ConfigurableListableBeanFactory beanFactory) { + ClassLoader beanClassLoader = beanFactory.getBeanClassLoader(); + List registrars = + new ArrayList<>(SpringFactoriesLoader.loadFactories(RuntimeHintsRegistrar.class, beanClassLoader)); + Arrays.stream(beanFactory.getBeanNamesForAnnotation(ImportRuntimeHints.class)).forEach(beanDefinitionName -> { + ImportRuntimeHints importRuntimeHints = beanFactory.findAnnotationOnBean(beanDefinitionName, ImportRuntimeHints.class); + if (importRuntimeHints != null) { + Class[] registrarClasses = importRuntimeHints.value(); + for (Class registrarClass : registrarClasses) { + logger.trace(LogMessage.format("Loaded [%s] registrar from annotated bean [%s]", registrarClass.getCanonicalName(), beanDefinitionName)); + RuntimeHintsRegistrar registrar = BeanUtils.instantiateClass(registrarClass); + registrars.add(registrar); + } + } + }); + return new RuntimeHintsRegistrarContribution(registrars, beanClassLoader); + } + + + static class RuntimeHintsRegistrarContribution implements BeanFactoryContribution { + + 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(BeanFactoryInitialization initialization) { + this.registrars.forEach(registrar -> { + logger.trace(LogMessage.format("Processing RuntimeHints contribution from [%s]", registrar.getClass().getCanonicalName())); + registrar.registerHints(initialization.generatedTypeContext().runtimeHints(), this.beanClassLoader); + }); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/generator/RuntimeHintsPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/generator/RuntimeHintsPostProcessorTests.java new file mode 100644 index 00000000000..cfd3bfe8699 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/generator/RuntimeHintsPostProcessorTests.java @@ -0,0 +1,182 @@ +/* + * 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.generator; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generator.DefaultGeneratedTypeContext; +import org.springframework.aot.generator.GeneratedType; +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.BeanDefinitionBuilder; +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 RuntimeHintsPostProcessor}. + * + * @author Brian Clozel + */ +class RuntimeHintsPostProcessorTests { + + private DefaultGeneratedTypeContext generationContext; + + private ApplicationContextAotGenerator generator; + + @BeforeEach + void setup() { + this.generationContext = createGenerationContext(); + this.generator = new ApplicationContextAotGenerator(); + } + + @Test + void shouldProcessRegistrarOnConfiguration() { + GenericApplicationContext applicationContext = createContext(ConfigurationWithHints.class); + this.generator.generateApplicationContext(applicationContext, this.generationContext); + assertThatSampleRegistrarContributed(); + } + + @Test + void shouldProcessRegistrarOnBeanMethod() { + GenericApplicationContext applicationContext = createContext(ConfigurationWithBeanDeclaringHints.class); + this.generator.generateApplicationContext(applicationContext, this.generationContext); + assertThatSampleRegistrarContributed(); + } + + @Test + void shouldProcessRegistrarInSpringFactory() { + GenericApplicationContext applicationContext = createContext(); + applicationContext.setClassLoader(new TestSpringFactoriesClassLoader()); + this.generator.generateApplicationContext(applicationContext, this.generationContext); + assertThatSampleRegistrarContributed(); + } + + @Test + void shouldRejectRuntimeHintsRegistrarWithoutDefaultConstructor() { + GenericApplicationContext applicationContext = createContext(ConfigurationWithIllegalRegistrar.class); + assertThatThrownBy(() -> this.generator.generateApplicationContext(applicationContext, this.generationContext)) + .isInstanceOf(BeanInstantiationException.class); + } + + private void assertThatSampleRegistrarContributed() { + Stream bundleHints = this.generationContext.runtimeHints().resources().resourceBundles(); + assertThat(bundleHints).anyMatch(bundleHint -> "sample".equals(bundleHint.getBaseName())); + } + + private GenericApplicationContext createContext(Class... configClasses) { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + AnnotationConfigUtils.registerAnnotationConfigProcessors(applicationContext); + for (Class configClass : configClasses) { + applicationContext.registerBeanDefinition(configClass.getSimpleName(), new RootBeanDefinition(configClass)); + } + applicationContext.registerBeanDefinition("runtimeHintsPostProcessor", + BeanDefinitionBuilder.rootBeanDefinition(RuntimeHintsPostProcessor.class, RuntimeHintsPostProcessor::new).getBeanDefinition()); + return applicationContext; + } + + private DefaultGeneratedTypeContext createGenerationContext() { + ClassName mainGeneratedType = ClassName.get("com.example", "Test"); + return new DefaultGeneratedTypeContext(mainGeneratedType.packageName(), packageName -> + GeneratedType.of(ClassName.get(packageName, mainGeneratedType.simpleName()))); + } + + + @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"); + } + + } + + + private static class TestSpringFactoriesClassLoader extends URLClassLoader { + + TestSpringFactoriesClassLoader() { + super(new URL[] {testLocation()}, RuntimeHintsPostProcessorTests.class.getClassLoader()); + } + + private static URL testLocation() { + try { + return new File("src/test/resources/org/springframework/context/annotation/runtimehints/").toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + } + +} diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/runtimehints/META-INF/spring.factories b/spring-context/src/test/resources/org/springframework/context/annotation/runtimehints/META-INF/spring.factories new file mode 100644 index 00000000000..fc129cfb4a7 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/runtimehints/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.context.generator.RuntimeHintsPostProcessorTests.SampleRuntimeHintsRegistrar \ No newline at end of file diff --git a/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsRegistrar.java new file mode 100644 index 00000000000..edcd06c431e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsRegistrar.java @@ -0,0 +1,42 @@ +/* + * 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.aot.hint; + +import org.springframework.lang.Nullable; + +/** + * Contract for registering {@link RuntimeHints} in a static fashion. + *

Implementations will contribute hints without any knowledge of the application context + * and can only use the given {@link ClassLoader} to conditionally contribute hints. + *

{@code RuntimeHintsRegistrar} can be declared as {@code spring.factories} entries; + * the registrar will be processed as soon as its declaration is found in the classpath. + * A standard no-arg constructor is required for implementations. + * + * @author Brian Clozel + * @since 6.0 + */ +@FunctionalInterface +public interface RuntimeHintsRegistrar { + + /** + * Contribute hints to the given {@link RuntimeHints} instance. + * @param hints the hints contributed so far for the application + * @param classLoader the classloader, or {@code null} if even the system ClassLoader isn't accessible + */ + void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader); + +}