Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring - Programmatically generate a set of beans

I have a Dropwizard application that needs to generate a dozen or so beans for each of the configs in a configuration list. Things like health checks, quartz schedulers, etc.

Something like this:

@Component class MyModule {     @Inject     private MyConfiguration configuration;      @Bean     @Lazy     public QuartzModule quartzModule() {         return new QuartzModule(quartzConfiguration());     }       @Bean     @Lazy     public QuartzConfiguration quartzConfiguration() {         return this.configuration.getQuartzConfiguration();     }      @Bean     @Lazy     public HealthCheck healthCheck() throws SchedulerException {         return this.quartzModule().quartzHealthCheck();     } } 

I have multiple instances of MyConfiguration that all need beans like this. Right now I have to copy and paste these definitions and rename them for each new configuration.

Can I somehow iterate over my configuration classes and generate a set of bean definitions for each one?

I would be fine with a subclassing solution or anything that is type safe without making me copy and paste the same code and rename the methods ever time I have to add a new service.

EDIT: I should add that I have other components that depend on these beans (they inject Collection<HealthCheck> for example.)

like image 543
noah Avatar asked Feb 06 '15 20:02

noah


People also ask

How do you create multiple beans in Spring?

Using @Component Annotation In this approach, we'll use the @Component annotation to create multiple beans that inherit their properties from the Person class. Now, we can just use the PersonOne or PersonTwo beans from the Spring container. Everywhere else, we can use the Person class bean.

How do I register beans in Spring boot programmatically?

You can implement the ApplicationContextInitializer to register the bean programmatically. It is essentially code that gets executed before the Spring application context gets completely created. In this class, you can call registerBean and register the bean programmatically.


2 Answers

So you need to declare new beans on-the-fly and inject them into Spring's application context as if they were just common beans, meaning they must be subject to proxying, post-processing, etc, i.e. they must be subject to Spring beans lifecycle.

Please see BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry() method javadocs. This is exactly what you are in need of, because it lets you modify Spring's application context after normal bean definitions have been loaded but before any single bean has been instantiated.

@Configuration public class ConfigLoader implements BeanDefinitionRegistryPostProcessor {      private final List<String> configurations;      public ConfigLoader() {         this.configurations = new LinkedList<>();         // TODO Get names of different configurations, just the names!         // i.e. You could manually read from some config file         // or scan classpath by yourself to find classes          // that implement MyConfiguration interface.         // (You can even hardcode config names to start seeing how this works)         // Important: you can't autowire anything yet,          // because Spring has not instantiated any bean so far!         for (String readConfigurationName : readConfigurationNames) {             this.configurations.add(readConfigurationName);         }     }      public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {         // iterate over your configurations and create the beans definitions it needs         for (String configName : this.configurations) {             this.quartzConfiguration(configName, registry);             this.quartzModule(configName, registry);             this.healthCheck(configName, registry);             // etc.         }     }      private void quartzConfiguration(String configName, BeanDefinitionRegistry registry) throws BeansException {         String beanName = configName + "_QuartzConfiguration";         BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(QuartzConfiguration.class).setLazyInit(true);          // TODO Add what the bean needs to be properly initialized         // i.e. constructor arguments, properties, shutdown methods, etc         // BeanDefinitionBuilder let's you add whatever you need         // Now add the bean definition with given bean name         registry.registerBeanDefinition(beanName, builder.getBeanDefinition());     }      private void quartzModule(String configName, BeanDefinitionRegistry registry) throws BeansException {         String beanName = configName + "_QuartzModule";         BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(QuartzModule.class).setLazyInit(true);          builder.addConstructorArgReference(configName + "_QuartzConfiguration"); // quartz configuration bean as constructor argument         // Now add the bean definition with given bean name         registry.registerBeanDefinition(beanName, builder.getBeanDefinition());     }      private void healthCheck(String configName, BeanDefinitionRegistry registry) throws BeansException {         String beanName = configName + "_HealthCheck";         BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(HealthCheck.class).setLazyInit(true);          // TODO Add what the bean needs to be properly initialized         // i.e. constructor arguments, properties, shutdown methods, etc         // BeanDefinitionBuilder let's you add whatever you need         // Now add the bean definition with given bean name         registry.registerBeanDefinition(beanName, builder.getBeanDefinition());     }      // And so on for other beans... } 

This effectively declares the beans you need and injects them into Spring's application context, one set of beans for each configuration. You have to rely on some naming pattern and then autowire your beans by name wherever needed:

@Service public class MyService {      @Resource(name="config1_QuartzConfiguration")     private QuartzConfiguration config1_QuartzConfiguration;      @Resource(name="config1_QuartzModule")     private QuartzModule config1_QuartzModule;      @Resource(name="config1_HealthCheck")     private HealthCheck config1_HealthCheck;      ...  } 

Notes:

  1. If you go by reading configuration names manually from a file, use Spring's ClassPathResource.getInputStream().

  2. If you go by scanning the classpath by yourself, I strongly recommend you use the amazing Reflections library.

  3. You have to manually set all properties and dependencies to each bean definition. Each bean definition is independant from other bean definitions, i.e. you cannot reuse them, set them one inside another, etc. Think of them as if you were declaring beans the old XML way.

  4. Check BeanDefinitionBuilder javadocs and GenericBeanDefinition javadocs for further details.

like image 148
fps Avatar answered Sep 24 '22 19:09

fps


You should be able to do something like this:

@Configuration public class MyConfiguration implements BeanFactoryAware {      private BeanFactory beanFactory;      @Override     public void setBeanFactory(BeanFactory beanFactory) {         this.beanFactory = beanFactory;     }      @PostConstruct     public void onPostConstruct() {         ConfigurableBeanFactory configurableBeanFactory = (ConfigurableBeanFactory) beanFactory;         for (..) {             // setup beans programmatically             String beanName= ..             Object bean = ..             configurableBeanFactory.registerSingleton(beanName, bean);         }      }  } 
like image 40
micha Avatar answered Sep 22 '22 19:09

micha