I am trying to create N number of beans of type DynamicStringMaker
in my code based on the configuration values provided as an array. This is intended to be packaged up and called by other applications, gated by a @ConditionalOnProperty
annotation.
I have the issue that following this StackOverflow answer I either need to find a way to always create the myBeansList
bean, even if it is not used or I would have to auto wire it in somewhere and never use it.
I have tried to use the @PostConstruct
annotation instead, but then the named beans are not created so I get a null pointer exception when trying to use it, or the app doesn't start (using required = true/false)
It working because I AutoWire in the array
// BeanMaking Class
package uk.co.iamsimonsmale.facades.DynmicString.autoconfigure;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import lombok.extern.slf4j.Slf4j;
import uk.co.iamsimonsmale.facades.DynmicString.DynamicStringMaker;
import uk.co.iamsimonsmale.facades.DynmicString.ConfigurationProperties.Multi;
import uk.co.iamsimonsmale.facades.DynmicString.ConfigurationProperties.Single;
@Configuration
@Slf4j
public class multiDynamicStrings {
@Autowired
private Multi multiConfig;
@Autowired
private ConfigurableListableBeanFactory beanFactory;
@Bean
public Map<String, DynamicStringMaker> makeMakers() {
Map<String, DynamicStringMaker> beanList = new HashMap<>();
log.info(multiConfig.lastName());
for (Single config : multiConfig.configs()) {
DynamicStringMaker newBean = new DynamicStringMaker(multiConfig.lastName(), config.firstName());
beanFactory.registerSingleton(config.beanName(), newBean);
}
return beanList;
}
}
# BeanConfig
uk.co.iamsimonsmale.dynamic-strings:
lastName: Smale
configs:
- beanName: simonBean
firstName: Simon
- beanName: griffinBean
firstName: Griffin
Beans' definitions should be registered earlier in the lifecycle of the Spring application context, before the bean instantiation phase. To do so, you can implement BeanDefinitionRegistryPostProcessor
interface as follows:
public class DynamicStringMakerBeanDefinitionRegistrar
implements BeanDefinitionRegistryPostProcessor {
public static final String PROPERTIES_PREFIX = "uk.co.iamsimonsmale.dynamic-strings";
private final Multi multi;
public DynamicStringMakerBeanDefinitionRegistrar(Environment environment) {
multi =
Binder.get(environment)
.bind(PROPERTIES_PREFIX, Bindable.of(Multi.class))
.orElseThrow(IllegalStateException::new);
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
throws BeansException {
multi.configs().forEach(config -> registerBeanDefinition(registry, config));
}
private void registerBeanDefinition(BeanDefinitionRegistry registry, Single config) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DynamicStringMaker.class);
beanDefinition.setInstanceSupplier(
() -> new DynamicStringMaker(multi.lastName(), config.firstName()));
registry.registerBeanDefinition(config.beanName(), beanDefinition);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {}
}
Since properties are needed before beans are instantiated, to register DynamicStringMaker
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 lifecycle, @Bean
methods should be marked as static in @Configuration
classes to avoid lifecycle issues, according to Spring documentation.
@Configuration
public class DynamicStringMakerBeanDefinitionRegistrarConfiguration {
@Bean
public static DynamicStringMakerBeanDefinitionRegistrar beanDefinitionRegistrar(Environment environment) {
return new DynamicStringMakerBeanDefinitionRegistrar(environment);
}
}
To use those beans, you can either inject one by a bean name or all of them as a collection.
For reference: Dynamically register Spring Beans based on properties