Piritz
Piritz

Reputation: 61

how to make Spring boot Dynamic bean creation?

Given a set of properties in an application.yaml

restClient:
    clients:
        client1:
            connection-timeout: PT10s 
            response-timeout: PT10s 
            user-agent: test
        client2:
            connection-timeout: PT10s 
            response-timeout: PT10s 
            user-agent: test2

I would like to generate dynamically those beans and be able to inject them as if they were defined manually in a @Configuration class.

I have tried to work out my own FactoryPostProcessor implementing BeanDefinitionRegistryPostProcessor (shown here and here) and though this could potentially work creating beans from a newly created DefaultRestClientBuilder using RestClient.builder(), I would lose all the auto configuration done by spring boot in RestClientAutoConfiguration class and all the metrics configured, also capabilities to use bundles.

I would like to achieve a hybrid solution where I could use the factoryPostProcessor and still depend on conditional annotations to receive the RestClient.Builder.

Thanks in advance.

UPDATE

For clarification, this will be used as part of a library, which means that will be part of an autoconfiguration. Also would be nice to make integration as clean as possible, which means that would like to avoid any @conditional annotation in services that will use the dynamic bean to block instantiation till the dynamic beans are binded.

Upvotes: 2

Views: 125

Answers (2)

Piritz
Piritz

Reputation: 61

Seems that I have managed to have some working example by creating a BeanDefinitionRegistryPostProcessor and creating them dynamically there by using RestClient builder definition.

@Bean
public static BeanDefinitionRegistryPostProcessor restClientBeanRegistrar() {
return new BeanDefinitionRegistryPostProcessor() {

  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    
  }

  @Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    
    RestClient.Builder builder = beanFactory.getBean(RestClient.Builder.class);

    RestClientProperties properties = Binder.get(beanFactory.getBean(Environment.class))
        .bind("restclient", Bindable.of(RestClientProperties.class))
            .orElseThrow(IllegalArgumentException::new);

    properties.getClients().forEach((clientName, clientConfig) -> {
      RestClient restClient = buildRestClient(builder, clientConfig);
      beanFactory.registerSingleton(clientName, restClient);
    });
  }

  private RestClient buildRestClient(RestClient.Builder builder, RestClientProperties.RestClientConfig config) {
    return builder
        .requestFactory(buildClientFactory(config))
        .build();
  }

  private ClientHttpRequestFactory buildClientFactory(RestClientProperties.RestClientConfig config) {
    var poolManager = PoolingHttpClientConnectionManagerBuilder.create()
        .setMaxConnPerRoute(config.connectionsPerRoute())
        .setMaxConnTotal(config.maxConnections())
        .build();

    return new HttpComponentsClientHttpRequestFactory(
        HttpClientBuilder.create()
            .setUserAgent(config.userAgent())
            .disableAutomaticRetries()
            .setDefaultRequestConfig(RequestConfig.custom()
                .setConnectionRequestTimeout(Timeout.of(config.connectionTimeout()))
                .setResponseTimeout(Timeout.of(config.responseTimeout()))
                .build())
            .setConnectionManager(poolManager)
            .evictIdleConnections(TimeValue.of(config.idledTimeout()))
            .build()
    );
  }
};  
}

This way all the clients seems to be injected properly

Updated This is what I have approached in order to

  1. define them and being able to reference them and make the names available for other services to inject.
  2. have beans instantiated and be able to use
public class RestClientBeanDefinition implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {

  
  private Environment environment;

  @Override
  public void setEnvironment(Environment environment) {
    this.environment = environment;
  }

  @Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    BeanDefinitionRegistryPostProcessor.super.postProcessBeanFactory(beanFactory);
  }

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

    YourProperties properties = Binder.get(environment)
        .bind("properties.prefix", Bindable.of(YourProperties .class))
        .orElseThrow(() -> new IllegalArgumentException(
            "No properties found"));

    properties.getClients().forEach((clientName, clientConfig) -> {
      final var genericBeanDefinition = new GenericBeanDefinition();
      genericBeanDefinition.setBeanClass(RestClient.class);
      genericBeanDefinition.setInstanceSupplier(() -> RestClient.builder().build());
      registry.registerBeanDefinition(clientName, genericBeanDefinition);
    });
  }
}

In this way you initially load them in context making them available from the beginning. Also triggers its auto configuration.

public class RestClientBeanPostProcessor implements BeanPostProcessor {

  private final ConfigurableListableBeanFactory beanFactory;
  private final YourProperties restClientProperties;
  private RestClient.Builder builder;
  
  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

    if (bean instanceof RestClient && restClientProperties.getClients().containsKey(beanName)) {
      builder = beanFactory.getBean(RestClient.Builder.class);
      
      
      bean = buildRestClient(restClientProperties.getClients().get(beanName));
    }

    return bean;
  }

}

This way you finally instantiate your bean

@Imran :pointUp

Upvotes: 2

Imran
Imran

Reputation: 6265

Updated Answer:

If you are creating dynamic beans from shared library OR want to avoid PostConstruct approach, go with @Piritz answer with BeanDefinitionRegistryPostProcessor which seems to be cleaner approach and potentially avoids circular dependency!!.


Original Answer:

Here is one way to create dynamic RestClients. There can be other optimized ways too but this is my take. Let me know your thoughts. FYI, an example GitHub repo and little bit more details are here.

application.yml

restClients:
  clients:
  - clientName: test1
    connectionTimeout: 6000
    responseTimeout: 6000
    userAgent: test1
  - clientName: test2
    connectionTimeout: 5000
    responseTimeout: 5000
    userAgent: test2

Let's load the configuration into ConfigProperties record.

DynamicRestBuilderProperties.java

import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.List;

@ConfigurationProperties(prefix = "rest-clients")
public record DynamicRestBuilderProperties(List<CustomClient> clients) {
    public record CustomClient(String clientName, int connectionTimeout, int responseTimeout, String userAgent) {
    }
}

Config Class(DemoConfig.java) where we can create dynamic beans. By using clone() on Autowired RestClient.Builder bean you can retain all spring default auto configuration when creating new ones.

@Configuration
@EnableConfigurationProperties(DynamicRestBuilderProperties.class)
public class DemoConfig {

    private static final Logger logger = LoggerFactory.getLogger(DemoConfig.class);
    @Autowired
    private DynamicRestBuilderProperties dynamicRestBuilderProperties;

    @Autowired
    private ConfigurableApplicationContext configurableApplicationContext;

    @Autowired
    private RestClient.Builder restClientBuilder;

    public DemoConfig() {
        logger.info("DemoConfig Initialized!!!!");
    }

    @PostConstruct
    public void init() {
        ConfigurableListableBeanFactory beanFactory = this.configurableApplicationContext.getBeanFactory();
        // iterate over properties and register new beans'
        for (DynamicRestBuilderProperties.CustomClient client : dynamicRestBuilderProperties.clients()) {
            RestClient tempClient = restClientBuilder.clone().requestFactory(getClientHttpRequestFactory(client.connectionTimeout(), client.responseTimeout())).defaultHeader("user-agent", client.userAgent()).build();
            beanFactory.autowireBean(tempClient);
            beanFactory.initializeBean(tempClient, client.clientName());
            beanFactory.registerSingleton(client.clientName(), tempClient);
            logger.info("{} bean created", client.clientName());
        }
    }

    private ClientHttpRequestFactory getClientHttpRequestFactory(int connectionTimeout, int responseTimeout) {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(responseTimeout);
        factory.setConnectTimeout(connectionTimeout);
        return factory;
    }
}

Now, you can use these dynamically created beans anywhere like following.

@Autowired
@Qualifier("test1")
private RestClient restClient;

@Autowired
@Qualifier("test2")
private RestClient restClient2;

restClient.get().uri("https://httpbin.org/user-agent").retrieve().body(String.class) //Should return test1

restClient2.get().uri("https://httpbin.org/user-agent").retrieve().body(String.class) //Should return test2

Upvotes: 0

Related Questions