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 0000000000..5950c7db5a
--- /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 extends RuntimeHintsRegistrar>[] 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 5e800e0a8e..5159e21cb6 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 0000000000..c28dea2b0f
--- /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 extends RuntimeHintsRegistrar>[] registrarClasses = importRuntimeHints.value();
+ for (Class extends RuntimeHintsRegistrar> 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 0000000000..cfd3bfe869
--- /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 0000000000..fc129cfb4a
--- /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 0000000000..edcd06c431
--- /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);
+
+}