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.
CodePudding user response:
ImportBeanDefinitionRegistrar seems to be what you actually need:
public class DynamicBeanRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
@Setter
private BeanFactory beanFactory;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
BeanProperties properties = beanFactory.getBean(BeanProperties.class);
for (String beanName : properties.getBeans()) {
if (registry.containsBeanDefinition(beanName)) {
continue;
}
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
.genericBeanDefinition(BeanClass.class,
() -> new BeanClass(beanName))
.getBeanDefinition();
registry.registerBeanDefinition(beanName, beanDefinition);
}
}
@Configuration
@EnableConfigurationProperties(BeanProperties.class)
static class BeanPropertiesConfiguration {
// spring parses nested classes first
}
}
@EnableConfigurationProperties(BeanProperties.class)
// !!!
@Import(DynamicBeanRegistrar.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();
}
}
CodePudding user response:
Since properties are needed before beans are instantiated, to register BeanClass
beans' definitions, @ConfigurationProperties
are unsuitable for this case. Instead, Binder API can be used to bind them programmatically.
BeanClass
beans' definitions can be registered in the implementation of BeanDefinitionRegistryPostProcessor
interface.
public class DynamicBeanDefinitionRegistrar implements BeanDefinitionRegistryPostProcessor {
private final List<String> beanNames;
public DynamicBeanDefinitionRegistrar(Environment environment) {
beanNames =
Binder.get(environment)
.bind("beans", 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 {}
}
Configuration class for the DynamicBeanDefinitionRegistrar
bean:
@Configuration
public class DynamicBeanDefinitionRegistrarConfiguration {
@Bean
public static DynamicBeanDefinitionRegistrar beanDefinitionRegistrar(Environment environment) {
return new DynamicBeanDefinitionRegistrar(environment);
}
}
Finally, 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
CodePudding user response:
Here is a description of what you want (in a slightly simplified version):
https://www.baeldung.com/spring-same-class-multiple-beans
You need to register you own Implementation of BeanFactoryPostProcessor
adding the functionality you need.
@Configuration
public class MyAppConfig {
@Bean
public CustomBeanFactoryPostProcessor beanFactoryPostProcessor() {
return new CustomBeanFactoryPostProcessor();
}
}
Using your own implementation you will be able to register the beans manually using ConfigurableListableBeanFactory
like this:
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
....
beanFactory.registerSingleton(name, bean);
....
}
After all you need to create a generic factory, which will be used by the processor to create the beans:
public class BeanClassFactory implements FactoryBean<BeanClass> {
/** This value is used by processor to run the method
<code>getSpecialObject</code> multiple times. */
@Autowired
protected BeanProperties beanProperties;
@Override
public Class<BeanClass> getObjectType() {
return BeanClass.class;
}
@Override
public BeanClass getObject() throws Exception {
// some generic instance, which won't be used.
// useful instances will be created later by getSpecialObject()
return new BeanClass();
}
public BeanClass getSpecialObject(String property) throws Exception {
... some logic based on provided property ....
return instance;
}
}
See the referenced baeldung article for some more examples.