Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create N number of beans with BeanDefinitionRegistryPostProcessor

I'm trying to create N number of beans dynamically using BeanDefinitionRegistryPostProcessor. Based off of this question, I opted to use BeanDefinitionRegistryPostProcessor for my use case.

I have the following defined in my application.yml:

app:
  downstream-services:
    rest:
      jsonPlaceHolder:
        url: https://jsonplaceholder.typicode.com/todos
        api-type: io.mateo.dynamicbeans.JsonPlaceHolderApi

Which gets wired up to a ConfigiruationProperties class here: https://github.com/ciscoo/dynamicbeans/blob/master/src/main/java/io/mateo/dynamicbeans/FeignConfigurationProperties.java

I then want to inject that ConfigiruationProperties class along with a factory bean that I defined here: https://github.com/ciscoo/dynamicbeans/blob/master/src/main/java/io/mateo/dynamicbeans/FeignClientAutoConfiguration.java

So now I have the following:

https://github.com/ciscoo/dynamicbeans/blob/master/src/main/java/io/mateo/dynamicbeans/FeignClientFactoryPostProcessor.java

@Component
public class FeignClientFactoryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    private final FeignConfigurationProperties properties;
    private final FeignClientFactory feignClientFactory;

    public FeignClientFactoryPostProcessor(FeignConfigurationProperties properties, FeignClientFactory feignClientFactory) {
        this.properties = properties;
        this.feignClientFactory = feignClientFactory;
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        properties.getDownstreamServices().getRest().forEach((beanName, props) -> makeClient(beanName, props, registry));
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // no-op
    }

    private void makeClient(String beanName, FeignClientProperties props, BeanDefinitionRegistry registry) {
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(props.getApiType());
        beanDefinition.setInstanceSupplier(() -> feignClientFactory.create(props));
        registry.registerBeanDefinition(beanName, beanDefinition);
    }
}

The single bean it should create is to injected in a service class here: https://github.com/ciscoo/dynamicbeans/blob/master/src/main/java/io/mateo/dynamicbeans/JsonPlaceHolderService.java

The problem I'm running into is:

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.mateo.dynamicbeans.FeignClientFactoryPostProcessor]: No default constructor found; nested exception is java.lang.NoSuchMethodException: io.mateo.dynamicbeans.FeignClientFactoryPostProcessor.<init>()
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:83) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1262) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    ... 17 common frames omitted
Caused by: java.lang.NoSuchMethodException: io.mateo.dynamicbeans.FeignClientFactoryPostProcessor.<init>()
    at java.base/java.lang.Class.getConstructor0(Class.java:3350) ~[na:na]
    at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2554) ~[na:na]
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:78) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    ... 18 common frames omitted

But when I remove the final keyword from the two properties and the defined constructor, I get a NullPointerException.

So how do I dynamically create N number of beans so that they will be available in time for any of my @Service classes to use?


I'm aware of https://spring.io/projects/spring-cloud-openfeign. I recreated my issue here to illustrate the same problem I'm having in a different project with dynamically creating SOAP clients.

Update: Doing the following changes: https://github.com/ciscoo/dynamicbeans/commit/4f16de9d03271025cd65d95932a3e854c0619c29, now I am able to accomplish my use case.

like image 599
Francisco Mateo Avatar asked Nov 24 '18 22:11

Francisco Mateo


People also ask

How do you inject multiple beans of the same type?

The default autowiring is by type, not by name, so when there is more than one bean of the same type, you have to use the @Qualifier annotation.

What is BeanDefinitionRegistryPostProcessor?

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor. Extension to the standard BeanFactoryPostProcessor SPI, allowing for the registration of further bean definitions before regular BeanFactoryPostProcessor detection kicks in.


1 Answers

As the answer to the question that you have linked to suggests, you can’t inject dependencies into a bean factory post-processor. Rather than injecting your configuration properties class, you’ll need to bind it programmatically. In Spring Boot 2.x, that is achieved using the Binder API:

The new Binder API can also be used outside of @ConfigurationProperties directly in your own code. For example, the following will bind to a List of PersonName objects:

List<PersonName> people = Binder.get(environment)
    .bind("my.property", Bindable.listOf(PersonName.class))   
    .orElseThrow(IllegalStateException::new);

The configuration source could be represented in YAML like this:

my:
  property:
  - first-name: Jane
    last-name: Doe
  - first-name: John
    last-name: Doe
like image 84
Andy Wilkinson Avatar answered Sep 30 '22 18:09

Andy Wilkinson