Search code examples
javaspringspring-boot

How to dynamically create N beans based on configuration


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)

This is a simple example of a more complex codebase that I have at work so it is a little noddy. Here it is not working as the bean method is never called

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

Solution

  • 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