diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistrar.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistrar.java index dd8ac536541..753840226db 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistrar.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistrar.java @@ -19,18 +19,20 @@ package org.springframework.beans.factory; import org.springframework.core.env.Environment; /** - * Contract for registering beans programmatically. - * - *

Typically imported with an {@link org.springframework.context.annotation.Import @Import} - * annotation on {@link org.springframework.context.annotation.Configuration @Configuration} - * classes. + * Contract for registering beans programmatically, typically imported with an + * {@link org.springframework.context.annotation.Import @Import} annotation on + * a {@link org.springframework.context.annotation.Configuration @Configuration} + * class. *

  * @Configuration
  * @Import(MyBeanRegistrar.class)
  * class MyConfiguration {
  * }
+ * Can also be applied to an application context via + * {@link org.springframework.context.support.GenericApplicationContext#register(BeanRegistrar...)}. + * * - *

The bean registrar implementation uses {@link BeanRegistry} and {@link Environment} + *

Bean registrar implementations use {@link BeanRegistry} and {@link Environment} * APIs to register beans programmatically in a concise and flexible way. *

  * class MyBeanRegistrar implements BeanRegistrar {
@@ -50,6 +52,10 @@ import org.springframework.core.env.Environment;
  *     }
  * }
* + *

A {@code BeanRegistrar} implementing {@link org.springframework.context.annotation.ImportAware} + * can optionally introspect import metadata when used in an import scenario, otherwise the + * {@code setImportMetadata} method is simply not being called. + * *

In Kotlin, it is recommended to use {@code BeanRegistrarDsl} instead of * implementing {@code BeanRegistrar}. * diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index da4e23a2c26..22cc4968659 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -66,7 +66,7 @@ final class ConfigurationClass { private final Map> importedResources = new LinkedHashMap<>(); - private final Set beanRegistrars = new LinkedHashSet<>(); + private final Map beanRegistrars = new LinkedHashMap<>(); private final Map importBeanDefinitionRegistrars = new LinkedHashMap<>(); @@ -222,11 +222,11 @@ final class ConfigurationClass { return this.importedResources; } - void addBeanRegistrar(BeanRegistrar beanRegistrar) { - this.beanRegistrars.add(beanRegistrar); + void addBeanRegistrar(String sourceClassName, BeanRegistrar beanRegistrar) { + this.beanRegistrars.put(sourceClassName, beanRegistrar); } - public Set getBeanRegistrars() { + public Map getBeanRegistrars() { return this.beanRegistrars; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index de14187568c..72776fe44a7 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -404,11 +404,11 @@ class ConfigurationClassBeanDefinitionReader { registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator)); } - private void loadBeanDefinitionsFromBeanRegistrars(Set registrars) { + private void loadBeanDefinitionsFromBeanRegistrars(Map registrars) { Assert.isInstanceOf(ListableBeanFactory.class, this.registry, "Cannot support bean registrars since " + this.registry.getClass().getName() + " does not implement BeanDefinitionRegistry"); - registrars.forEach(registrar -> registrar.register(new BeanRegistryAdapter(this.registry, + registrars.values().forEach(registrar -> registrar.register(new BeanRegistryAdapter(this.registry, (ListableBeanFactory) this.registry, this.environment, registrar.getClass()), this.environment)); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 5198d6aca7b..659047e8929 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -602,7 +602,11 @@ class ConfigurationClassParser { else if (candidate.isAssignable(BeanRegistrar.class)) { Class candidateClass = candidate.loadClass(); BeanRegistrar registrar = (BeanRegistrar) BeanUtils.instantiateClass(candidateClass); - configClass.addBeanRegistrar(registrar); + AnnotationMetadata metadata = currentSourceClass.getMetadata(); + if (registrar instanceof ImportAware importAware) { + importAware.setImportMetadata(metadata); + } + configClass.addBeanRegistrar(metadata.getClassName(), registrar); } else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { // Candidate class is an ImportBeanDefinitionRegistrar -> diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index 78d6e5b39ee..c11e9721037 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -114,6 +114,7 @@ import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.NameAllocator; import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -122,6 +123,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; /** * {@link BeanFactoryPostProcessor} used for bootstrapping processing of @@ -197,7 +199,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo @SuppressWarnings("NullAway.Init") private List propertySourceDescriptors; - private Set beanRegistrars = new LinkedHashSet<>(); + private Map beanRegistrars = new LinkedHashMap<>(); @Override @@ -443,7 +445,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo } this.reader.loadBeanDefinitions(configClasses); for (ConfigurationClass configClass : configClasses) { - this.beanRegistrars.addAll(configClass.getBeanRegistrars()); + this.beanRegistrars.putAll(configClass.getBeanRegistrars()); } alreadyParsed.addAll(configClasses); processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end(); @@ -846,13 +848,13 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo private static final String ENVIRONMENT_VARIABLE = "environment"; - private final Set beanRegistrars; + private final Map beanRegistrars; private final ConfigurableListableBeanFactory beanFactory; private final AotServices aotProcessors; - public BeanRegistrarAotContribution(Set beanRegistrars, ConfigurableListableBeanFactory beanFactory) { + public BeanRegistrarAotContribution(Map beanRegistrars, ConfigurableListableBeanFactory beanFactory) { this.beanRegistrars = beanRegistrars; this.beanFactory = beanFactory; this.aotProcessors = AotServices.factoriesAndBeans(this.beanFactory).load(BeanRegistrationAotProcessor.class); @@ -935,13 +937,32 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo private CodeBlock generateRegisterCode() { Builder code = CodeBlock.builder(); - for (BeanRegistrar beanRegistrar : this.beanRegistrars) { - code.addStatement("new $T().register(new $T(($T)$L, $L, $L, $T.class, $L), $L)", beanRegistrar.getClass(), + Builder metadataReaderFactoryCode = null; + NameAllocator nameAllocator = new NameAllocator(); + for (Map.Entry beanRegistrarEntry : this.beanRegistrars.entrySet()) { + BeanRegistrar beanRegistrar = beanRegistrarEntry.getValue(); + String beanRegistrarName = nameAllocator.newName(StringUtils.uncapitalize(beanRegistrar.getClass().getSimpleName())); + code.addStatement("$T $L = new $T()", beanRegistrar.getClass(), beanRegistrarName, beanRegistrar.getClass()); + if (beanRegistrar instanceof ImportAware) { + if (metadataReaderFactoryCode == null) { + metadataReaderFactoryCode = CodeBlock.builder(); + metadataReaderFactoryCode.addStatement("$T metadataReaderFactory = new $T()", + MetadataReaderFactory.class, CachingMetadataReaderFactory.class); + } + code.beginControlFlow("try") + .addStatement("$L.setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())", + beanRegistrarName, beanRegistrarEntry.getKey()) + .nextControlFlow("catch ($T ex)", IOException.class) + .addStatement("throw new $T(\"Failed to read metadata for '$L'\", ex)", + IllegalStateException.class, beanRegistrarEntry.getKey()) + .endControlFlow(); + } + code.addStatement("$L.register(new $T(($T)$L, $L, $L, $T.class, $L), $L)", beanRegistrarName, BeanRegistryAdapter.class, BeanDefinitionRegistry.class, BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, ENVIRONMENT_VARIABLE, beanRegistrar.getClass(), CUSTOMIZER_MAP_VARIABLE, ENVIRONMENT_VARIABLE); } - return code.build(); + return (metadataReaderFactoryCode == null ? code.build() : metadataReaderFactoryCode.add(code.build()).build()); } private CodeBlock generateInitDestroyMethods(String beanName, AbstractBeanDefinition beanDefinition, diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java index ebaba3b7cdb..ab54eb1b620 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java @@ -500,6 +500,22 @@ public class ConfigurationClassPostProcessorAotContributionTests { }); } + @Test + void applyToWhenIsImportAware() { + BeanFactoryInitializationAotContribution contribution = getContribution(CommonAnnotationBeanPostProcessor.class, + ImportAwareBeanRegistrarConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + assertThat(freshContext.getBean(ClassNameHolder.class).className()) + .isEqualTo(ImportAwareBeanRegistrarConfiguration.class.getName()); + freshContext.close(); + }); + } + @SuppressWarnings("unchecked") private void compile(BiConsumer, Compiled> result) { MethodReference methodReference = beanFactoryInitializationCode.getInitializers().get(0); @@ -561,6 +577,31 @@ public class ConfigurationClassPostProcessorAotContributionTests { } } + @Import(ImportAwareBeanRegistrar.class) + public static class ImportAwareBeanRegistrarConfiguration { + } + + public static class ImportAwareBeanRegistrar implements BeanRegistrar, ImportAware { + + @Nullable + private AnnotationMetadata importMetadata; + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(ClassNameHolder.class, spec -> spec.supplier(context -> + new ClassNameHolder(this.importMetadata == null ? null : this.importMetadata.getClassName()))); + } + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.importMetadata = importMetadata; + } + + public @Nullable AnnotationMetadata getImportMetadata() { + return this.importMetadata; + } + } + static class Foo { } @@ -576,6 +617,8 @@ public class ConfigurationClassPostProcessorAotContributionTests { } + public record ClassNameHolder(@Nullable String className) {} + private @Nullable BeanFactoryInitializationAotContribution getContribution(Class... types) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/beanregistrar/BeanRegistrarConfigurationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/beanregistrar/BeanRegistrarConfigurationTests.java index 46c9118b681..ac33e4490b6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/beanregistrar/BeanRegistrarConfigurationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/beanregistrar/BeanRegistrarConfigurationTests.java @@ -24,12 +24,14 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.testfixture.beans.factory.GenericBeanRegistrar; +import org.springframework.context.testfixture.beans.factory.ImportAwareBeanRegistrar; import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Bar; import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Baz; import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Foo; import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Init; import org.springframework.context.testfixture.context.annotation.registrar.BeanRegistrarConfiguration; import org.springframework.context.testfixture.context.annotation.registrar.GenericBeanRegistrarConfiguration; +import org.springframework.context.testfixture.context.annotation.registrar.ImportAwareBeanRegistrarConfiguration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -82,4 +84,13 @@ public class BeanRegistrarConfigurationTests { assertThat(beanDefinition.getResolvableType().resolveGeneric(0)).isEqualTo(GenericBeanRegistrar.Foo.class); } + @Test + void beanRegistrarWithImportAware() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(ImportAwareBeanRegistrarConfiguration.class); + context.refresh(); + assertThat(context.getBean(ImportAwareBeanRegistrar.ClassNameHolder.class).className()) + .isEqualTo(ImportAwareBeanRegistrarConfiguration.class.getName()); + } + } diff --git a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java index c96b05b3b1a..b8987430832 100644 --- a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -46,6 +46,8 @@ import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcess import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.context.testfixture.beans.factory.ImportAwareBeanRegistrar; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar; import org.springframework.core.DecoratingProxy; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; @@ -627,6 +629,22 @@ class GenericApplicationContextTests { context.close(); } + @Test + void beanRegistrar() { + GenericApplicationContext context = new GenericApplicationContext(); + context.register(new SampleBeanRegistrar()); + context.refresh(); + assertThat(context.getBean(SampleBeanRegistrar.Bar.class).foo()).isEqualTo(context.getBean(SampleBeanRegistrar.Foo.class)); + } + + @Test + void importAwareBeanRegistrar() { + GenericApplicationContext context = new GenericApplicationContext(); + context.register(new ImportAwareBeanRegistrar()); + context.refresh(); + assertThat(context.getBean(ImportAwareBeanRegistrar.ClassNameHolder.class).className()).isNull(); + } + private MergedBeanDefinitionPostProcessor registerMockMergedBeanDefinitionPostProcessor(GenericApplicationContext context) { MergedBeanDefinitionPostProcessor bpp = mock(); diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/ImportAwareBeanRegistrar.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/ImportAwareBeanRegistrar.java new file mode 100644 index 00000000000..8915117cf9d --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/ImportAwareBeanRegistrar.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2025 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.testfixture.beans.factory; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotationMetadata; + +public class ImportAwareBeanRegistrar implements BeanRegistrar, ImportAware { + + @Nullable + private AnnotationMetadata importMetadata; + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(ClassNameHolder.class, spec -> spec.supplier(context -> + new ClassNameHolder(this.importMetadata == null ? null : this.importMetadata.getClassName()))); + } + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.importMetadata = importMetadata; + } + + public @Nullable AnnotationMetadata getImportMetadata() { + return this.importMetadata; + } + + public record ClassNameHolder(@Nullable String className) {} +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ImportAwareBeanRegistrarConfiguration.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ImportAwareBeanRegistrarConfiguration.java new file mode 100644 index 00000000000..7b4b21b4615 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ImportAwareBeanRegistrarConfiguration.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2025 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.testfixture.context.annotation.registrar; + +import org.springframework.context.annotation.Import; +import org.springframework.context.testfixture.beans.factory.ImportAwareBeanRegistrar; + +@Import(ImportAwareBeanRegistrar.class) +public class ImportAwareBeanRegistrarConfiguration { +}