Reputation: 61
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
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
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
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