5

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.

George
  • 2,820
  • 4
  • 29
  • 56
  • I think you must consider writing a Configurable Interceptor or Filter to return these headers – shazin Jan 09 '18 at 05: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? – George Jan 09 '18 at 06:12

3 Answers3

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

Aliaksei Stadnik
  • 1,692
  • 3
  • 15
  • 32
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 {

    }
}
Josh Long
  • 91
  • 1
  • 1
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