Spring - Programmatically generate a set of beans
Asked Answered
F

7

48

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.)

Festinate answered 6/2, 2015 at 20:13 Comment(1)
Either you need to register bean definitions by BeanDefinitionRegistryPostProcessor or do some context hierarchy magic (separate contexts for your modules where the dependencies can register themselves in the parent context) or just do a service lookup instead of letting spring to inject your dependencies (i.e. ApplicationContext#getBeansOfType).Artichoke
F
-6

The "best" approach I could come up with was to wrap all of my Quartz configuration and schedulers in 1 uber bean and wire it all up manually, then refactor the code to work with the uber bean interface.

The uber bean creates all the objects that I need in its PostConstruct, and implements ApplicationContextAware so it can auto-wire them. It's not ideal, but it was the best I could come up with.

Spring simply does not have a good way to dynamically add beans in a typesafe way.

Festinate answered 6/2, 2015 at 20:14 Comment(1)
Just because you don't want to give up your bounty doesn't mean your own answer is the right one.Buff
D
81

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

    @Override
    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.

Dermatosis answered 16/2, 2015 at 21:16 Comment(1)
Is there any way to reuse one bean (dynamically created) inside another bean while dynamic creation?Stipe
D
19

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

}
Dorman answered 6/2, 2015 at 20:23 Comment(3)
This will not work because I need to construct beans, have them injected, and use them to create other beans.Festinate
Works very well with SpringBoot 2.xPham
Thanks a lot for this you saved me a lot of time! I posted a similar question under "load properties in class that implements ImportBeanDefinitionRegistrar" but your solution worked for me. I just implemented BeanFactoryAware and this took care of it.Walkway
C
8

I'll just chip in here. Others have mentioned that you need to create a bean, into which your config is injected. That bean will then use your config to create other beans and insert them into the context (which you'll also need injecting in one form or another).

What I don't think anyone else has picked up on, is that you've said other beans will be dependant upon these dynamically created beans. This means that your dynamic bean factory must be instantiated before the dependant beans. You can do this (in annotations world) using

@DependsOn("myCleverBeanFactory")

As for what type of object your clever bean factory is, others have recommended better ways of doing this. But if I remember correctly you can actually do it something like this in the old spring 2 world :

public class MyCleverFactoryBean implements ApplicationContextAware, InitializingBean {
  @Override
  public void afterPropertiesSet() {
    //get bean factory from getApplicationContext()
    //cast bean factory as necessary
    //examine your config
    //create beans
    //insert beans into context
   } 

..

Conto answered 16/2, 2015 at 22:0 Comment(0)
C
7

Just expanding on Michas answer - his solution works if I set it up like this:

public class ToBeInjected {

}

public class PropertyInjected {

    private ToBeInjected toBeInjected;

    public ToBeInjected getToBeInjected() {
        return toBeInjected;
    }

    @Autowired
    public void setToBeInjected(ToBeInjected toBeInjected) {
        this.toBeInjected = toBeInjected;
    }

}

public class ConstructorInjected {
    private final ToBeInjected toBeInjected;

    public ConstructorInjected(ToBeInjected toBeInjected) {
        this.toBeInjected = toBeInjected;
    }

    public ToBeInjected getToBeInjected() {
        return toBeInjected;
    }

}

@Configuration
public class BaseConfig implements BeanFactoryAware{

    private ConfigurableBeanFactory beanFactory;

    protected ToBeInjected toBeInjected;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = (ConfigurableBeanFactory) beanFactory;
    }

    @PostConstruct
    public void addCustomBeans() {
        toBeInjected = new ToBeInjected();
        beanFactory.registerSingleton(this.getClass().getSimpleName() + "_quartzConfiguration", toBeInjected);
    }

    @Bean
    public ConstructorInjected test() {
        return new ConstructorInjected(toBeInjected);
    }

    @Bean
    public PropertyInjected test2() {
        return new PropertyInjected();
    }

}

One thing to note is that I am creating the custom beans as attributes of the configuration class and initialising them in the @PostConstruct method. This way I have the object registered as a bean (so @Autowire and @Inject works as expected) and I can later use the same instance in constructor injection for beans that require it. The attribute visibility is set to protected, so that subclasses can use the created objects.

As the instance that we are holding is not actually the Spring proxy, some problems may occur (aspects not firing etc.). It may actually be a good idea to retrieve the bean after registering it, as in:

toBeInjected = new ToBeInjected();
String beanName = this.getClass().getSimpleName() + "_quartzConfiguration";
beanFactory.registerSingleton(beanName, toBeInjected);
toBeInjected = beanFactory.getBean(beanName, ToBeInjected.class);
Chasseur answered 13/2, 2015 at 7:50 Comment(0)
B
1

You need to create a base configuration class which is extended by all your Configuration classes. Then, you can iterate over all the configuration classes as follows:

// Key - name of the configuration class
// value - the configuration object
Map<String, Object> configurations = applicationContext.getBeansWithAnnotation(Configuration.class);
Set<String> keys = configurations.keySet();
for(String key: keys) {
    MyConfiguration conf = (MyConfiguration) configurations.get(key);

    // Implement the logic to use this configuration to create other beans.
}
Brainsick answered 13/2, 2015 at 6:42 Comment(0)
P
0

Here are some more examples on how to generate beans programmatically.

I used to create beans like this

private void createBean(final DefaultListableBeanFactory factory, final String name, final Object bean) {
    //apply BeanPostProcessors
    final var instance = factory.initializeBean(bean, name);
    //creates bean definition, autowire dependent beans if such exists, setPropertyValues
    factory.autowireBeanProperties(instance, AbstractBeanDefinition.AUTOWIRE_BY_TYPE, true);
    //add bean to DefaultSingletonBeanRegistry registeredSingletons and singletonObjects
    factory.registerSingleton(name, instance);
}

And it was quite fine. All dependencies were resolved and properties were set. Besides, in some cases it's quite enough to call only SingletonBeanRegistry#registerSingleton(String, Object).

However, when creating beans with generics I faced NoUniqueBeanDefinitionException exception ("expected single matching bean but found ") when they are autowired.

The solution was to register bean definition and set RootBeanDefinition.targetType explicitly. Finally,

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ResolvableType;

import java.util.function.Supplier;

@Configuration
class CreateBeanConfig {
    CreateBeanConfig(final DefaultListableBeanFactory factory) {
        createBean(factory, "testIntBean", (Supplier<Integer>)() -> 1, Supplier.class, Integer.class);
        createBean(factory, "testStringBean", (Supplier<String>)() -> "testString", Supplier.class, String.class);
    }

    private void createBean(
            final DefaultListableBeanFactory factory,
            final String name,
            final Object bean,
            final Class<?> type,
            final Class<?>... generics
    ) {
        final var bd = new RootBeanDefinition();
        bd.setTargetType(ResolvableType.forClassWithGenerics(type, generics));
        bd.setAutowireCandidate(true);
        bd.setScope(BeanDefinition.SCOPE_SINGLETON);
        bd.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        factory.registerBeanDefinition(name, bd);
        //apply BeanPostProcessors, autowire dependent beans if such exists, setPropertyValues
        final var instance = factory.configureBean(bean, name);
        //add bean to DefaultSingletonBeanRegistry registeredSingletons and singletonObjects
        factory.registerSingleton(name, instance);
    }
}

Besides, AbstractAutowireCapableBeanFactory#configureBean(Object,String) requires bean definition created otherwise it throws exception. So, if you want to use this method you need bean definition anyway.

If NoUniqueBeanDefinitionException is not reproduced in your Spring version for the first approach, feel free to use it.

Pyknic answered 21/1 at 9:48 Comment(0)
F
-6

The "best" approach I could come up with was to wrap all of my Quartz configuration and schedulers in 1 uber bean and wire it all up manually, then refactor the code to work with the uber bean interface.

The uber bean creates all the objects that I need in its PostConstruct, and implements ApplicationContextAware so it can auto-wire them. It's not ideal, but it was the best I could come up with.

Spring simply does not have a good way to dynamically add beans in a typesafe way.

Festinate answered 6/2, 2015 at 20:14 Comment(1)
Just because you don't want to give up your bounty doesn't mean your own answer is the right one.Buff

© 2022 - 2024 — McMap. All rights reserved.