简体   繁体   中英

Creating a collection of beans in Spring using @Configuration

How can I create a collection of beans that will be properly managed by Spring using a class with a @Configuration annotation.

I would like to do something like this:

@Configuration
public Config {
    @Autowired
    private SomeConfiguration config;

    @Bean
    public List<MyBean> myBeans() {
        List<MyBean> beans = new ArrayList<MyBean>();
        for (Device device : config.getDevices()) {
            beans.add(new MyBean(device));
        }
        return beans;
    }
}

But the MyBean instances aren't post processed. So their @Autowired methods are not called, the beans are not registered as mbeans and etc. The list is however accessible so that I can autowire a List of MyBean objects.

I cannot use something like:

@Configuration
public Config {
    @Autowired
    private SomeConfiguration config;

    @Bean
    public MyBean myBean1() { ... }

    @Bean
    public MyBean myBean2() { ... }
}

Since the number of MyBean instances are not known before runtime. The reason I want to do this is because we are controlling a physical machine that have a variable amount of components. And I want to have one bean per component.

I'm currently achieving our goal by using a BeanFactoryPostProcessor like this:

@Component
public class MyBeansFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Autowired
    private SomeConfiguration config;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeanException {
        for (Device device : config.getDevices()) {
            createAndRegister(BeanDefinitionRegistry) beanFactory, device);
        }
    }

    private void createAndRegister(BeanDefinitionRegistry registry, Device device) {
        register.registerBeanDefinition("device" + device.getId(), BeanDefinitionBuilder.genericBeanDefinition(MyBean.class).addConstructorArgValue(device).getBeanDefinition());
    }
}

But this just feels like a really ugly hack.

In order to inject your MyBean list try @Resource instead of @Autowired. for eg

@Resource
public List<MyBean> myBeans

It's not possible using @Configuration to define more than one bean per method (AFAIK). So you will have to contnue using a BFPP or use ApplicationContect.getAutowireCapableBeanFactory().autowire(object);

I believe that another option in this case is to use @PostConstruct in the following manner:

@Configuration
public Config {

    @Autowired
    private SomeConfiguration config;

    List<MyBean> beans = new ArrayList<MyBean>();

    @Bean
    public List<MyBean> myBeans() {     

        return beans;
    }

    @PostConstruct
    public void init() {
        for (Device device : config.getDevices()) {
            beans.add(new MyBean(device));
        }   
    }
}

The @PostConstruct annotation is useful for initializing properties. It guarantees that the annotated method will only be called once when the bean is created.

Correct me if I'm wrong

You can use the ConfigurableListableBeanFactory which supports SmartLifecycle so if you register the bean before your app is fully initialized it will call start() for you and other post processing steps.

However - if you call beanFactory.registerSingleton after spring has initialized you will manually need to call start() - on the bright side though you bean is still fully wired into the lifecycle management and spring will call stop() for you when the application context is shutdown.

@Autowired
private ConfigurableListableBeanFactory beanFactory;

@Bean
public List<MyBean> myBeansList() {

    List<MyBean> mylist; // Construct your list dynamically

    while(myCondition) {
        MyBean bean;
        // Manually register each instance with Spring
        beanFactory.registerSingleton("unique-name-for-this-bean",bean);
    }

    // Return your list as a bean so you can still autowire the list of beans
    // but each bean has already been manually added to the context
    return mylist;
}

MyBeans are not post processed as they are created with new and not initialised by the Spring container.

You need to use prototype beans, have an instance of a new bean per request made by a component.

You will need to tag your MyBean(Device device) bean declaration like

@Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)

Then call that instead of using new where you populate beans .

I've ended up extending the ArrayList.

@Configuration
public class Config {
    @Autowired
    private SomeConfiguration config;

    @Bean
    public List<MyBean> myBeans() {
        List<MyBean> beans = new MyBeanList();
        for (final Device device : config.getDevices()) {
            beans.add(new MyBean(device));
        }
        return beans;
    }

    private static class MyBeanList extends ArrayList<MyBean> {
        @PostConstruct
        public void init() {
            this.forEach(bean -> bean.init());
        }

        @PreDestroy
        public void close() {
            this.forEach(bean -> bean.close());
        }
    }
}

This is, of course, hacky but feels a less uglier than the questioners approach.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM