From 0f761df2ab6491aff1aca6c86b41433c3ca9dcc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 2 Apr 2026 10:56:07 +0200 Subject: [PATCH] Polish BeanRegistrar Javadoc and add tests for non-invocation semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revise the BeanRegistrar Javadoc to document the two distinct usage modes: @Configuration/@Import and programmatic GenericApplicationContext setup. Clarify that implementations are not Spring components (requiring a no-arg constructor and no dependency injection), and detail the ordering guarantees for each mode. Add missing tests Signed-off-by: Stéphane Nicoll --- .../beans/factory/BeanRegistrar.java | 66 ++++++++++++++----- .../context/DeferredBeanRegistrar.java | 46 ------------- .../BeanRegistrarConfigurationTests.java | 45 ++++++++++++- .../registrar/ComponentBeanRegistrar.java | 35 ++++++++++ .../registrar/ConfigurationBeanRegistrar.java | 49 ++++++++++++++ 5 files changed, 176 insertions(+), 65 deletions(-) delete mode 100644 spring-context/src/main/java/org/springframework/context/DeferredBeanRegistrar.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ComponentBeanRegistrar.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ConfigurationBeanRegistrar.java 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 4ba835973c82..7ae466b8af87 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,21 +19,9 @@ import org.springframework.core.env.Environment; /** - * 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...)}. - * + * Contract for registering beans programmatically. Implementations use the + * {@link BeanRegistry} and {@link Environment} to register beans: * - *

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

  * class MyBeanRegistrar implements BeanRegistrar {
  *
@@ -52,9 +40,55 @@
  *     }
  * }
* + *

{@code BeanRegistrar} implementations are not Spring components: they must have + * a no-arg constructor and cannot rely on dependency injection or any other + * component-model feature. They can be used in two distinct ways depending on the + * application context setup. + * + *

With the {@code @Configuration} model

+ * + *

A {@code BeanRegistrar} must be imported via + * {@link org.springframework.context.annotation.Import @Import} on a + * {@link org.springframework.context.annotation.Configuration @Configuration} class: + * + *

+ * @Configuration
+ * @Import(MyBeanRegistrar.class)
+ * class MyConfiguration {
+ * }
+ * + *

This is the only mechanism that triggers bean registration in the annotation-based + * configuration model. Annotating an implementation with {@code @Configuration} or + * {@code @Component}, or returning an instance from a {@code @Bean} method, registers + * it as a bean but does not invoke its + * {@link #register(BeanRegistry, Environment) register} method. + * + *

When imported, the registrar is invoked in the order it is encountered during + * configuration class processing. It can therefore check for and build on beans that + * have already been defined, but has no visibility into beans that will be registered + * by classes processed later. + * + *

Programmatic usage

+ * + *

A {@code BeanRegistrar} can also be applied directly to a + * {@link org.springframework.context.support.GenericApplicationContext}: + * + *

+ * GenericApplicationContext context = new GenericApplicationContext();
+ * context.register(new MyBeanRegistrar());
+ * context.registerBean("myBean", MyBean.class);
+ * context.refresh();
+ * + *

This mode is primarily intended for fully programmatic application context setups. + * Registrars applied this way are invoked before any {@code @Configuration} class is + * processed. They can therefore observe beans registered programmatically (e.g., via + * {@link org.springframework.context.support.GenericApplicationContext#registerBean(Class)}), + * but will not see any beans defined in {@code @Configuration} classes + * also registered with the context. + * *

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. + * can optionally introspect import metadata when used in an import scenario; otherwise + * the {@code setImportMetadata} method is not 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/DeferredBeanRegistrar.java b/spring-context/src/main/java/org/springframework/context/DeferredBeanRegistrar.java deleted file mode 100644 index 77ed04f73837..000000000000 --- a/spring-context/src/main/java/org/springframework/context/DeferredBeanRegistrar.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2002-present 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; - -import org.springframework.beans.factory.BeanRegistrar; -import org.springframework.beans.factory.BeanRegistry; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.env.Environment; - -/** - * A variant of {@link BeanRegistrar} which aims to be invoked - * at the end of the bean registration phase, coming after regular - * bean definition reading and configuration class processing. - * - *

This allows for seeing all user-registered beans, potentially - * reacting to their presence. The {@code containsBean} methods on - * {@link BeanRegistry} will provide reliable answers, independent - * of the order of user bean registration versus {@code BeanRegistrar} - * import/registration. - * - * @author Juergen Hoeller - * @since 7.1 - * @see #register(BeanRegistry, Environment) - * @see BeanRegistry#containsBean(String) - * @see BeanRegistry#containsBean(Class) - * @see BeanRegistry#containsBean(ParameterizedTypeReference) - * @see org.springframework.context.support.GenericApplicationContext#register( BeanRegistrar...) - * @see org.springframework.context.annotation.Import - */ -public interface DeferredBeanRegistrar extends BeanRegistrar { - -} 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 e9c5f21bc596..c0ffebbe88fe 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 @@ -34,19 +34,28 @@ 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.ComponentBeanRegistrar; +import org.springframework.context.testfixture.context.annotation.registrar.ComponentBeanRegistrar.IgnoredFromComponent; import org.springframework.context.testfixture.context.annotation.registrar.ConditionalBeanRegistrarConfiguration; +import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar; +import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar.BeanBeanRegistrar; +import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar.IgnoredFromBean; +import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar.IgnoredFromConfiguration; import org.springframework.context.testfixture.context.annotation.registrar.GenericBeanRegistrarConfiguration; import org.springframework.context.testfixture.context.annotation.registrar.ImportAwareBeanRegistrarConfiguration; import org.springframework.context.testfixture.context.annotation.registrar.MultipleBeanRegistrarsConfiguration; import org.springframework.context.testfixture.context.annotation.registrar.TestBeanConfiguration; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link BeanRegistrar} imported by @{@link org.springframework.context.annotation.Configuration}. * * @author Sebastien Deleuze + * @author Stephane Nicoll */ class BeanRegistrarConfigurationTests { @@ -63,6 +72,36 @@ void beanRegistrar() { assertThat(beanDefinition.getDescription()).isEqualTo("Custom description"); } + @Test + void beanRegistrarIgnoreBeans() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigurationBeanRegistrar.class); + assertThatNoException().isThrownBy(() -> context.getBean(ConfigurationBeanRegistrar.class)); + assertThatNoException().isThrownBy(() -> context.getBean(BeanBeanRegistrar.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(IgnoredFromConfiguration.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(IgnoredFromBean.class)); + } + + @Test + void beanRegistrarWithClasspathScanningIgnoreBeans() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.scan("org.springframework.context.testfixture.context.annotation.registrar"); + context.refresh(); + + assertThatNoException().isThrownBy(() -> context.getBean(ConfigurationBeanRegistrar.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(IgnoredFromConfiguration.class)); + + assertThatNoException().isThrownBy(() -> context.getBean(BeanBeanRegistrar.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(IgnoredFromBean.class)); + + assertThatNoException().isThrownBy(() -> context.getBean(ComponentBeanRegistrar.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(IgnoredFromComponent.class)); + } + @Test void beanRegistrarWithProfile() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); @@ -110,16 +149,16 @@ void multipleBeanRegistrars() { } @Test - void programmaticBeanRegistrarWithConditionNotMet() { + void programmaticBeanRegistrarIsInvokedBeforeConfigurationClassPostProcessor() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - context.register(new ConditionalBeanRegistrar()); context.register(TestBeanConfiguration.class); + context.register(new ConditionalBeanRegistrar()); context.refresh(); assertThat(context.containsBean("myTestBean")).isFalse(); } @Test - void programmaticBeanRegistrarWithConditionMet() { + void programmaticBeanRegistrarHandlesProgrammaticRegisteredBean() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(new ConditionalBeanRegistrar()); context.registerBean("testBean", TestBean.class); diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ComponentBeanRegistrar.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ComponentBeanRegistrar.java new file mode 100644 index 000000000000..7f6ba15a13cd --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ComponentBeanRegistrar.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-present 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.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +public class ComponentBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(IgnoredFromComponent.class); + } + + + public record IgnoredFromComponent() {} + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ConfigurationBeanRegistrar.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ConfigurationBeanRegistrar.java new file mode 100644 index 000000000000..5093e43390fb --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ConfigurationBeanRegistrar.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-present 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.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +@Configuration +public class ConfigurationBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(IgnoredFromConfiguration.class); + } + + @Bean + BeanBeanRegistrar beanBeanRegistrar() { + return new BeanBeanRegistrar(); + } + + public static class BeanBeanRegistrar implements BeanRegistrar { + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(IgnoredFromBean.class); + } + } + + + public record IgnoredFromConfiguration() {} + + public record IgnoredFromBean() {} +}