Home > Software design >  Dynamically register Spring Beans based on properties
Dynamically register Spring Beans based on properties

Time:01-08

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.

  • Related