J'ai une application qui a un certain nombre de paramètres de source de données répertoriés dans application.properties. J'ai un @ConfigurationProperties
classe qui charge ces paramètres. Maintenant, je veux prendre les valeurs de cette classe ConfigurationProperties
et les utiliser pour créer des beans DataSource à la volée. J'ai essayé d'utiliser @PostConstruct
et implémentant BeanFactoryPostProcessor
. Cependant, avec BeanFactoryPostProcessor
, le traitement semble arriver trop tôt - avant que ma classe ConfigurationProperties
n'ait été remplie. Comment puis-je lire les propriétés et créer DataSource
beans à la volée avec Spring Boot?
Voici à quoi ressemble mon application.properties:
ds.clients[0]=client1|jdbc:db2://server/client1
ds.clients[1]=client2,client3|jdbc:db2://server/client2
ds.clients[2]=client4|jdbc:db2://server/client4
ds.clients[3]=client5|jdbc:db2://server/client5
Et ma classe ConfigurationProperties:
@Component
@ConfigurationProperties(prefix = "ds")
public class DataSourceSettings {
public static Map<String, String> CLIENT_DATASOURCES = new LinkedHashMap<>();
private List<String> clients = new ArrayList<>();
public List<String> getClients() {
return clients;
}
public void setClients(List<String> clients) {
this.clients = clients;
}
@PostConstruct
public void configure() {
for (String client : clients) {
// extract client name
String[] parts = client.split("\\|");
String clientName = parts[0];
String url = parts[1];
// client to datasource mapping
String dsName = url.substring(url.lastIndexOf("/") + 1);
if (clientName.contains(",")) {
// multiple clients with same datasource
String[] clientList = clientName.split(",");
for (String c : clientList) {
CLIENT_DATASOURCES.put(c, dsName);
}
} else {
CLIENT_DATASOURCES.put(clientName, dsName);
}
}
}
À la fin de cette @PostConstruct
, j'aimerais créer un BasicDataSource
avec ces paramètres et l'ajouter au ApplicationContext. Cependant, si j'essaie de le faire en implémentant BeanFactoryPostProcessor
et en implémentant postProcessBeanFactory
, la propriété clients
est nulle, tout comme le CLIENT_DATASOURCES
que j'ai rempli avec @PostConstruct
.
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
System.out.println("clients: " + CLIENT_DATASOURCES);
}
Quelle est la meilleure façon de créer des sources de données à la volée avec Spring Boot?
Que diriez-vous de créer vos beans et de demander à Boot de lui injecter des valeurs?
Quelque chose comme
@Bean
@ConfigurationProperties("ds.client1")
public DataSource dataSource() {
DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("ds.client2")
public DataSource dataSource() {
DataSourceBuilder.create().build();
}
Ensuite, tout paramètre dans le ds.client1
l'espace de noms appartient à la première source de données (c'est-à-dire ds.client1.password
est le mot de passe de la source de données pour ce DataSource
).
Mais peut-être que vous ne savez pas combien de sources de données vous aurez? Cela devient plus compliqué, surtout si vous devez injecter ces sources de données dynamiques dans d'autres objets. Si vous avez seulement besoin de les rechercher par leur nom, vous pouvez les enregistrer vous-même en tant que singletons. Voici un exemple qui fonctionne
@ConfigurationProperties(prefix = "ds")
public class DataSourceSettings implements BeanFactoryAware {
private List<String> clients = new ArrayList<>();
private BeanFactory beanFactory;
public List<String> getClients() {
return clients;
}
public void setClients(List<String> clients) {
this.clients = clients;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@PostConstruct
public void configure() {
Map<String, String> clientDataSources = new HashMap<String, String>();
for (String client : clients) {
// extract client name
String[] parts = client.split("\\|");
String clientName = parts[0];
String url = parts[1];
// client to datasource mapping
String dsName = url.substring(url.lastIndexOf("/") + 1);
if (clientName.contains(",")) {
// multiple clients with same datasource
String[] clientList = clientName.split(",");
for (String c : clientList) {
clientDataSources.put(c, url);
}
}
else {
clientDataSources.put(clientName, url);
}
}
Assert.state(beanFactory instanceof ConfigurableBeanFactory, "wrong bean factory type");
ConfigurableBeanFactory configurableBeanFactory = (ConfigurableBeanFactory) beanFactory;
for (Map.Entry<String, String> entry : clientDataSources.entrySet()) {
DataSource dataSource = createDataSource(entry.getValue());
configurableBeanFactory.registerSingleton(entry.getKey(), dataSource);
}
}
private DataSource createDataSource(String url) {
return DataSourceBuilder.create().url(url).build();
}
}
Notez que ces beans sont niquement disponibles par recherche de nom de bean. Faites-moi savoir si cela fonctionne pour vous.
J'ai créé un exemple de projet sur github pour démontrer votre cas d'utilisation.
https://github.com/lhotari/dynamic-datasources
J'ai implémenté un ImportBeanDefinitionRegistrar pour ajouter les beans. Vous pouvez obtenir la configuration en implémentant EnvironmentAware . Il pourrait y avoir d'autres façons d'atteindre votre objectif, mais c'est la façon dont j'ai utilisé dans GspAutoConfiguration pour enregistrer les beans de manière dynamique. GspAutoConfiguration rend Grails GSP disponible dans les applications Spring Boot.
Voici la classe de configuration appropriée dans l'exemple de source de données dynamique: https://github.com/lhotari/dynamic-datasources/blob/master/src/main/groovy/sample/DynamicDataSourcesConfiguration.Java
package sample;
import Java.util.HashMap;
import Java.util.Map;
import Java.util.Map.Entry;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.bind.PropertiesConfigurationFactory;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import org.springframework.validation.BindException;
@Configuration
public class DynamicDataSourcesConfiguration implements ImportBeanDefinitionRegistrar, EnvironmentAware {
private ConfigurableEnvironment environment;
private static Map<String, Object> defaultDsProperties = new HashMap<String, Object>() {
{
put("suppressClose", true);
put("username", "sa");
put("password", "");
put("driverClassName", "org.h2.Driver");
}
};
@Override
public void setEnvironment(Environment environment) {
this.environment = (ConfigurableEnvironment)environment;
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
DataSourceSettings settings = resolveSettings();
for (Entry<String, String> entry : settings.clientDataSources().entrySet()) {
createDsBean(registry, entry.getKey(), entry.getValue());
}
}
private void createDsBean(BeanDefinitionRegistry registry, String beanName, String jdbcUrl) {
GenericBeanDefinition beanDefinition = createBeanDefinition(SingleConnectionDataSource.class);
beanDefinition.getPropertyValues().addPropertyValues(defaultDsProperties).addPropertyValue("url", jdbcUrl);
registry.registerBeanDefinition(beanName, beanDefinition);
}
private GenericBeanDefinition createBeanDefinition(Class<?> beanClass) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(beanClass);
beanDefinition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_NO);
return beanDefinition;
}
private DataSourceSettings resolveSettings() {
DataSourceSettings settings = new DataSourceSettings();
PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(settings);
factory.setTargetName("ds");
factory.setPropertySources(environment.getPropertySources());
factory.setConversionService(environment.getConversionService());
try {
factory.bindPropertiesToTarget();
}
catch (BindException ex) {
throw new FatalBeanException("Could not bind DataSourceSettings properties", ex);
}
return settings;
}
}