web-dev-qa-db-fra.com

Comment créer des beans par programmation dans Spring Boot?

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?

22
Matt Raible

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.

21
Stephane Nicoll

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;
    }

}
10
Lari Hotari