6

How do I dynamically define beans based on the application.yml file?

For example, the YAML file looks like this:

service:
   host: http://localhost:8080/
   account:
     url: /account
     content-type: application/json
   registry:
     url: /registry
     content-type: application/xml

And this would dynamically create two HttpHeaders with the Content-Type header set.

Here's how I define the beans now:

@Bean
public HttpHeaders accountHeaders(
    @Value("${service.account.content-type}") String contentType
) {
    HttpHeaders headers = new HttpHeaders();
    headers.set(HttpHeaders.CONTENT_TYPE, contentType);
    return headers;
}

@Bean
public HttpHeaders registryHeaders(
    @Value("${service.registry.content-type}") String contentType
) {
    HttpHeaders headers = new HttpHeaders();
    headers.set(HttpHeaders.CONTENT_TYPE, contentType);
    return headers;
}

If I need to add more endpoints, I would need to copy and paste these beans, which I would like to avoid.

Note: these dynamic beans do not require any other beans. I'm not sure if that makes a difference. It just needs to load the configuration.

2
  • I think you must consider writing a Configurable Interceptor or Filter to return these headers Commented Jan 9, 2018 at 5:52
  • I don't think filters can be applied here since these are meant for external requests using restTemplate. I've never used an interceptor. Any references? Commented Jan 9, 2018 at 6:12

3 Answers 3

9

You can inject all properties as described below (not sure how to do it with your current properties structure, spring allows really anvanced features regarding properties injection, additional examples here)

@ConfigurationProperties(prefix = "yourPrefix")
public class CustomProperties {

  private final Map<String, String> properties = new HashMap<>();

  @Autowired 
  private ApplicationContext applicationContext;      

  @PostConstruct
  public void init() {
    AutowireCapableBeanFactory beanFactory = this.applicationContext.getAutowireCapableBeanFactory();
    // iterate over properties and register new beans
  }

}

You can register beans manually with something like

beanFactory.registerSingleton("beanName", bean);

Additional examples of dynamic bean registration here here

Sign up to request clarification or add additional context in comments.

Comments

9

There are a couple of options:

  • Use programmatic ("functional") bean registration. In this way, registering a bean is a function and you can use for-loops and if/else, etc. The example by Aliaksei demonstrates this, sort of. I usually use an ApplicationContextInitializer registered in with a SpringApplicationBuilder() (instead of SpringApplication.run(..)).
  • you could use a ImportBeanDefinitionRegistrar. Implement it and then register beans using BeanDefinitions. Import that class with @Import(MyIbdr.class).
package com.example.dynabeans;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;

import java.util.UUID;

@SpringBootApplication
public class DynabeansApplication {

    public static void main(String[] args) {
        SpringApplication.run(DynabeansApplication.class, args);
    }

}

class Foo {

    private final String id = UUID.randomUUID().toString();

    @Override
    public String toString() {
        return "Foo{" + id + "}";
    }
}


@Component
class FooListener {

    private final Log log = LogFactory.getLog(getClass());

    FooListener(Foo[] foos) {
        log.info("there are " + foos.length + " " + Foo.class.getName() + " instances.");
    }

}

@Component
class LoopyBeanRegistrar implements BeanDefinitionRegistryPostProcessor {

    private final Log log = LogFactory.getLog(getClass());
    private final int max = (int) (Math.random() * 100);

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        log.info("registering " + max + " beans.");
        for (int i = 0; i < max; i++) {
            BeanDefinitionBuilder gdb = BeanDefinitionBuilder.genericBeanDefinition(Foo.class, () -> new Foo());
            AbstractBeanDefinition abd = gdb.getBeanDefinition();
            BeanDefinitionHolder holder = new BeanDefinitionHolder(abd, Foo.class.getName() + '#' + i, new String[0]);
            BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }
}

Comments

6

Testing it with the Environment and looks like it's working correctly. You have to externalize your registar in a Configuration to Inject the env though. The Binder is not mandatory here. env.getProperty() would have worked the same way.

@Configuration
public class DynamicBeansConfiguration {

    @Bean
    public BeanDefinitionRegistrar beanDefinitionRegistrar(Environment environment) {
        return new BeanDefinitionRegistrar(environment);
    }

    public class BeanDefinitionRegistrar implements BeanDefinitionRegistryPostProcessor {
        private Environment environment;

        public BeanDefinitionRegistrar(Environment environment) {
            this.environment = environment;
        }

        @Override
        public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

            List<Developer> developers = Binder.get(environment)
                    .bind("developers", Bindable.listOf(Developer.class))
                    .orElseThrow(IllegalStateException::new);

            developers.forEach(developer -> {
                GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
                beanDefinition.setBeanClass(Developer.class);
                beanDefinition.setInstanceSupplier(() -> new Developer(developer.getName()));
                registry.registerBeanDefinition(developer.getName(), beanDefinition);
            });
        }

        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        }
    }
}

application.properties:

developers=John,Jack,William

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.