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:
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user