2

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.

2 Answers 2

2

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

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

1 Comment

This is good. I like it!!. Glad its resolved for you. I will try this solution in my project where my current approach with PostConstruct is causing circular dependency which I really want to avoid.
0

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

6 Comments

Hey @imran, thanks for your answer. Yes that would work if I would use this as part of the configuration of an app, due to the cdi lifecycle that checks for @Configuration classes and then for @Services. Sorry for not giving a full picture on what I need. Plan is to include this as part of a library that will be used in other projects to allow dynamic bean creation. Since libraries use autoconfiguration loading, this are loaded on later stages therefore if client definition is used in a service which is scaned before, will fail with a bean of type X could not be found as not registered
This would be the main purpose to implement BeanDefinitionRegistryPostProcessor , to register them in really early stages
Note that I would like to leave it as simple as possible without having to use Conditional in normal services to wait for dependency to be created
@Piritz So if I understand correctly, in your case DemoConfig is loaded from dependency library then in that case, in your @Service class add a @Autowired dependency on one of the Bean from the DemoConfig class then it will make sure DemoConfig is initialized with all beans before Service class is. that bean can be anything, doesn't have to be dynamic one. Problem might be is, this can bring circular dependency issue. If you could reproduce the issue in sample repo I gave, it will help to find an solution which can be simple.
Hey @Imran, sorry for the delay. Purpose is to have this library autoconfigured as a pure spring boot style. Due to spring boot lifecycle autoconfigure seems to happen after component scan. Here you can find a library definition and here small usage of the lib. You will see how this fails due to scanning first service and trying to find a bean of Rest client type.
|

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.