I'm using the latest Spring Boot version and trying to dynamically create n number of beans based upon what is defined in the application.yaml file. I would then like to inject these beans into other classes based upon the bean name.
The code below is a much simplified example of what I am trying to achieve. The auto configuration would normally be part of a spring boot starter library so the number of beans needed to be registered is unknown.
@Slf4j
@Value
public class BeanClass {
private final String name;
public void logName() {
log.info("Name: {}", name);
}
}
@Component
@RequiredArgsConstructor
public class ServiceClass {
private final BeanClass fooBean;
private final BeanClass barBean;
public void log() {
fooBean.logName();
barBean.logName();
}
}
@Value
@ConfigurationProperties
public class BeanProperties {
private final List<String> beans;
}
@Configuration
public class AutoConfiguration {
// Obviously not correct
@Bean
public List<BeanClass> beans(final BeanProperties beanProperties) {
return beanProperties.getBeans().stream()
.map(BeanClass::new)
.collect(Collectors.toList());
}
}
@EnableConfigurationProperties(BeanProperties.class)
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
final ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);
final ServiceClass service = context.getBean(ServiceClass.class);
service.log();
}
}
beansToMake:
- fooBean
- barBean
I've tried multiple suggestions on google but nothing works and seems outdated. I'm hoping a new feature of Spring makes this straight forward.
You can implement BeanDefinitionRegistryPostProcessor interface to register BeanClass beans' definitions as follows:
public class DynamicBeanDefinitionRegistrar implements BeanDefinitionRegistryPostProcessor {
public static final String PROPERTIES_PREFIX = "beans";
private final List<String> beanNames;
public DynamicBeanDefinitionRegistrar(Environment environment) {
beanNames =
Binder.get(environment)
.bind(PROPERTIES_PREFIX, Bindable.listOf(String.class))
.orElseThrow(IllegalStateException::new);
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
throws BeansException {
beanNames.forEach(
beanName -> {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(BeanClass.class);
beanDefinition.setInstanceSupplier(() -> new BeanClass(beanName));
registry.registerBeanDefinition(beanName, beanDefinition);
});
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {}
}
Since properties are needed before beans are instantiated, to register BeanClass beans' definitions, @ConfigurationProperties are unsuitable for this case. Instead, Binder API is used to bind them programmatically.
Because BeanFactoryPostProcessor objects, in general, must be instantiated very early in the container lifecycle, @Bean methods should be marked as static in @Configuration classes to avoid lifecycle issues, according to Spring documentation.
@Configuration
public class DynamicBeanDefinitionRegistrarConfiguration {
@Bean
public static DynamicBeanDefinitionRegistrar beanDefinitionRegistrar(Environment environment) {
return new DynamicBeanDefinitionRegistrar(environment);
}
}
As a result, all beans you define in application.yml, are registered as BeanClass beans:
beans:
- fooBean
- barBean
For reference: Create N number of beans with BeanDefinitionRegistryPostProcessor, Spring Boot Dynamic Bean Creation From Properties File
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With