9

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.

Cisco
  • 20,972
  • 5
  • 38
  • 60

1 Answers1

11

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
Andy Wilkinson
  • 108,729
  • 24
  • 257
  • 242
  • That worked out perfectly, thank you! One side question, is it recommended that the `BeanDefinitionRegistryPostProcessor` be marked as `@Component` or defined as a `@Bean` within in `@Configuration` class? – Cisco Nov 25 '18 at 15:57
  • 3
    I would define it as a `static` `@Bean` method in a `@Configuration` class. Declaring the method as `static` means that the bean method can be called without instantiating its class. This will prevent the creation of your post-processor (which needs to happen very early) from causing anything also to be created earlier than wanted. – Andy Wilkinson Nov 25 '18 at 17:29
  • What is the recommended way to get hold of `Environment` in the post processor? – Martin Tarjányi Jan 19 '23 at 15:58
  • 1
    @MartinTarjányi As shown in [the code](https://github.com/ciscoo/dynamicbeans/commit/4f16de9d03271025cd65d95932a3e854c0619c29) that's linked at the end of the question, you can implement `EnvironmentAware`. I can't recall if you can use constructor injection for the `Environment`. You may want to try that as an alternative as, if it works, it would allow your post-processor to be immutable. – Andy Wilkinson Jan 20 '23 at 08:36
  • Ah, missed that. Thanks! Constructor injection works as well. – Martin Tarjányi Jan 20 '23 at 08:50