Introduce first-class support for programmatic bean registration

This commit introduces a new BeanRegistrar interface that can be
implemented to register beans programmatically in a concise and
flexible way.

Those bean registrar implementations are typically imported with
an `@Import` annotation on `@Configuration` classes.

See BeanRegistrarConfigurationTests for a concrete example.

See gh-18353
This commit is contained in:
Sébastien Deleuze
2025-03-06 18:51:09 +01:00
parent aeaf52ee96
commit 496be9ca98
14 changed files with 1356 additions and 8 deletions

View File

@@ -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.
@@ -16,6 +16,7 @@
package org.springframework.context.annotation;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
@@ -24,6 +25,7 @@ import java.util.function.Predicate;
import javax.lang.model.element.Modifier;
import jakarta.annotation.PostConstruct;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Nested;
@@ -37,7 +39,10 @@ import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanRegistrar;
import org.springframework.beans.factory.BeanRegistry;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
@@ -54,6 +59,7 @@ import org.springframework.context.testfixture.context.annotation.SimpleConfigur
import org.springframework.context.testfixture.context.generator.SimpleComponent;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.DefaultPropertySourceFactory;
import org.springframework.core.test.tools.Compiled;
@@ -74,8 +80,9 @@ import static org.assertj.core.api.Assertions.entry;
* @author Phillip Webb
* @author Stephane Nicoll
* @author Sam Brannen
* @author Sebastien Deleuze
*/
class ConfigurationClassPostProcessorAotContributionTests {
public class ConfigurationClassPostProcessorAotContributionTests {
private final TestGenerationContext generationContext = new TestGenerationContext();
@@ -439,6 +446,135 @@ class ConfigurationClassPostProcessorAotContributionTests {
}
}
@Nested
public class BeanRegistrarTests {
@Test
void applyToWhenHasDefaultConstructor() throws NoSuchMethodException {
BeanFactoryInitializationAotContribution contribution = getContribution(DefaultConstructorConfiguration.class);
assertThat(contribution).isNotNull();
contribution.applyTo(generationContext, beanFactoryInitializationCode);
Constructor<Foo> fooConstructor = Foo.class.getDeclaredConstructor();
compile((initializer, compiled) -> {
GenericApplicationContext freshContext = new GenericApplicationContext();
initializer.accept(freshContext);
freshContext.refresh();
assertThat(freshContext.getBean(Foo.class)).isNotNull();
assertThat(RuntimeHintsPredicates.reflection().onConstructorInvocation(fooConstructor))
.accepts(generationContext.getRuntimeHints());
freshContext.close();
});
}
@Test
void applyToWhenHasInstanceSupplier() {
BeanFactoryInitializationAotContribution contribution = getContribution(InstanceSupplierConfiguration.class);
assertThat(contribution).isNotNull();
contribution.applyTo(generationContext, beanFactoryInitializationCode);
compile((initializer, compiled) -> {
GenericApplicationContext freshContext = new GenericApplicationContext();
initializer.accept(freshContext);
freshContext.refresh();
assertThat(freshContext.getBean(Foo.class)).isNotNull();
assertThat(generationContext.getRuntimeHints().reflection().getTypeHint(Foo.class)).isNull();
freshContext.close();
});
}
@Test
void applyToWhenHasPostConstructAnnotationPostProcessed() {
BeanFactoryInitializationAotContribution contribution = getContribution(CommonAnnotationBeanPostProcessor.class,
PostConstructConfiguration.class);
assertThat(contribution).isNotNull();
contribution.applyTo(generationContext, beanFactoryInitializationCode);
compile((initializer, compiled) -> {
GenericApplicationContext freshContext = new GenericApplicationContext();
initializer.accept(freshContext);
freshContext.refresh();
Init init = freshContext.getBean(Init.class);
assertThat(init).isNotNull();
assertThat(init.initialized).isTrue();
assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(Init.class, "postConstruct"))
.accepts(generationContext.getRuntimeHints());
freshContext.close();
});
}
private void compile(BiConsumer<Consumer<GenericApplicationContext>, Compiled> result) {
MethodReference methodReference = beanFactoryInitializationCode.getInitializers().get(0);
beanFactoryInitializationCode.getTypeBuilder().set(type -> {
ArgumentCodeGenerator argCodeGenerator = ArgumentCodeGenerator
.of(ListableBeanFactory.class, "applicationContext.getBeanFactory()")
.and(ArgumentCodeGenerator.of(Environment.class, "applicationContext.getEnvironment()"));
CodeBlock methodInvocation = methodReference.toInvokeCodeBlock(argCodeGenerator,
beanFactoryInitializationCode.getClassName());
type.addModifiers(Modifier.PUBLIC);
type.addSuperinterface(ParameterizedTypeName.get(Consumer.class, GenericApplicationContext.class));
type.addMethod(MethodSpec.methodBuilder("accept").addModifiers(Modifier.PUBLIC)
.addParameter(GenericApplicationContext.class, "applicationContext")
.addStatement(methodInvocation)
.build());
});
generationContext.writeGeneratedContent();
TestCompiler.forSystem().with(generationContext).compile(compiled ->
result.accept(compiled.getInstance(Consumer.class), compiled));
}
@Configuration
@Import(DefaultConstructorBeanRegistrar.class)
public static class DefaultConstructorConfiguration {
}
public static class DefaultConstructorBeanRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
registry.registerBean(Foo.class);
}
}
@Configuration
@Import(InstanceSupplierBeanRegistrar.class)
public static class InstanceSupplierConfiguration {
}
public static class InstanceSupplierBeanRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
registry.registerBean(Foo.class, spec -> spec.supplier(context -> new Foo()));
}
}
@Configuration
@Import(PostConstructBeanRegistrar.class)
public static class PostConstructConfiguration {
}
public static class PostConstructBeanRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
registry.registerBean(Init.class);
}
}
static class Foo {
}
static class Init {
boolean initialized = false;
@PostConstruct
void postConstruct() {
initialized = true;
}
}
}
private @Nullable BeanFactoryInitializationAotContribution getContribution(Class<?>... types) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

View File

@@ -0,0 +1,72 @@
/*
* 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.annotation.beanregistrar;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanRegistrar;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@link BeanRegistrar} imported by @{@link org.springframework.context.annotation.Configuration}.
*
* @author Sebastien Deleuze
*/
public class BeanRegistrarConfigurationTests {
@Test
void beanRegistrar() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BeanRegistrarConfiguration.class);
assertThat(context.getBean(Bar.class).foo()).isEqualTo(context.getBean(Foo.class));
assertThatThrownBy(() -> context.getBean(Baz.class)).isInstanceOf(NoSuchBeanDefinitionException.class);
assertThat(context.getBean(Init.class).initialized).isTrue();
BeanDefinition beanDefinition = context.getBeanDefinition("bar");
assertThat(beanDefinition.getScope()).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE);
assertThat(beanDefinition.isLazyInit()).isTrue();
assertThat(beanDefinition.getDescription()).isEqualTo("Custom description");
}
@Test
void beanRegistrarWithProfile() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(BeanRegistrarConfiguration.class);
context.getEnvironment().addActiveProfile("baz");
context.refresh();
assertThat(context.getBean(Baz.class).message()).isEqualTo("Hello World!");
}
@Test
void scannedFunctionalConfiguration() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.scan("org.springframework.context.testfixture.context.annotation.registrar");
context.refresh();
assertThat(context.getBean(Bar.class).foo()).isEqualTo(context.getBean(Foo.class));
assertThatThrownBy(() -> context.getBean(Baz.class).message()).isInstanceOf(NoSuchBeanDefinitionException.class);
assertThat(context.getBean(Init.class).initialized).isTrue();
}
}