7

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.

sooty1892
  • 327
  • 1
  • 3
  • 13

3 Answers3

8

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 BeanDefinitionPostProcessor objects must be instantiated very early in the container lifecycle, @Bean methods should be marked as static in @Configuration classes, according to Spring documentation.

@Configuration
public class DynamicBeanDefinitionRegistrarConfiguration {
  @Bean
  public static DynamicBeanDefinitionRegistrar beanDefinitionRegistrar(Environment environment) {
    return new DynamicBeanDefinitionRegistrar(environment);
  }
}

As 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

Toni
  • 3,296
  • 2
  • 13
  • 34
0

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. See the referenced baeldung article for some more examples.

30thh
  • 10,861
  • 6
  • 32
  • 42
  • the problem with this approach is that BeanProperties hasn't been created at the time BeanFactoryPostProcessor is run (since it runs before bean creation), so I can't get hold of the bean names. Do you know of a workaround for this? – sooty1892 Jan 08 '23 at 08:24
  • It must be possible to inject the properties into the factory bean (the instance of the FactoryBean). The BeanFactoryPostProcessor will be able to read the values. It works similar to the parsing of the Qualifier annotation in the references article. – 30thh Jan 08 '23 at 09:34
0

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();
    }

}

Andrey B. Panfilov
  • 4,324
  • 2
  • 12
  • 18