1
votes

I am trying to dynamically register Beans in Spring Boot, however the order of how the beans are created always result in a NoSuchBeanDefinitionException if it try to autowire one of the dynamic beans.

My setup consists of two projects, one spring-boot-starter project and the actual spring-boot application.

The actual application registers a BeanDefinitionRegistryPostProcessor that adds the bean definitions. The instances itself are constructed via another bean defined in the starter project that itself takes another bean as dependency.

In order to consume the dynamically registered bean, I created a class annotated with @Component and defined a constructor expecting said bean as parameter. When I debug the application by setting @Autowired(required=false), I can see that the constructor of my component is called before the dynamic bean has been created. Moreover, not even the factory bean has been created at that time.

Added @DependsOn with the factory bean's name to component resulted the factory being created first, but not the dynamic beans.

Setting @DependsOn with the dynamic bean's name works, but that doesn't seem to be the correct way to solve this issue.

Why is Spring creating my beans in the wrong order and what can I do solve this?

EDIT:

I was able to reproduce the issue in a sample repository:
https://github.com/maveeee/spring-dynamic-bean-demo/

2

2 Answers

1
votes

You can use the @Order annotation which defines the sort order for an annotated component or bean.

Take into account that before Spring 4.0, this annotation was used only for the AspectJ execution order. After Spring 4.0, it is supported the ordering of injected components to a collection. Thus, Spring will inject the auto-wired beans of the same type based on their order value.

For example:

interface IBean {
    String getName();
}

public class BeanX implements IBean {
    public BeanX() {}

    @Override
    public String getName() {
        return "BeanX";
    }
}

public class BeanY implements IBean {
    public BeanY() {}

    @Override
    public String getName() {
        return "BeanY";
    }
}

@Component
public class RandomComponent {
    @Autowired
    private List<IBean> beans;

    @PostConstruct
    public void getBeanValues() {
        System.out.println("\n---@Bean---\n");
        for (IBean b : beans) {
            System.out.println(b.getName());
        }
    }

    @Bean
    @Order(1)
    public IBean getBeanX() {
        return new BeanX();
    }

    @Bean
    @Order(0)
    public IBean getBeanY() {
        return new BeanY();
    }
}

Will print:

---@Bean---

BeanY
BeanX

Because BeanY has higher precedence (0, lower value) over BeanX (higher value, 1).

GitHub Demo

Related articles:

0
votes

I figured out that my issue resulted from how I created the bean definition. I was using a GenericBeanDefinition instead of a RootBeanDefinition. Using the later allowed me to use setTargetType() instead of setBeanClass() which immediately resolved the issue and resulted in Spring figuring out the correct order to create the beans so that I could inject the dynamically created bean via @Autowired.

Before:

        var identifier = ...    // Some String identifying the bean
        var clazz = ...         // Some class object coming from a dependency

        var beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(clazz);
        beanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
        beanDefinition.setAutowireCandidate(true);
        beanDefinition.setFactoryBeanName(CONTRACT_FACTORY_BEAN_NAME);
        beanDefinition.setFactoryMethodName(CONTRACT_FACTORY_METHOD_NAME);

        registry.registerBeanDefinition(identifier, beanDefinition);

After:

        var identifier = ...    // Some String identifying the bean
        var clazz = ...         // Some class object coming from a dependency

        var beanDefinition = new RootBeanDefinition();
        beanDefinition.setTargetType(clazz);
        beanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
        beanDefinition.setAutowireCandidate(true);
        beanDefinition.setFactoryBeanName(CONTRACT_FACTORY_BEAN_NAME);
        beanDefinition.setFactoryMethodName(CONTRACT_FACTORY_METHOD_NAME);

        registry.registerBeanDefinition(identifier, beanDefinition);

I will update the sample code in the repository for further reference.