【问题标题】:How do I create beans programmatically in Spring Boot?如何在 Spring Boot 中以编程方式创建 bean?
【发布时间】:2014-08-06 12:19:40
【问题描述】:

我有一个应用程序,它在 application.properties 中列出了许多数据源设置。我有一个 @ConfigurationProperties 类来加载这些设置。现在我想从这个ConfigurationProperties 类中获取值并使用它们即时创建DataSource bean。我试过使用@PostConstruct 并实现BeanFactoryPostProcessor。然而,对于BeanFactoryPostProcessor,处理似乎发生在早期——在我的ConfigurationProperties 类被填充之前。如何使用 Spring Boot 即时读取属性并创建 DataSource bean?

这是我的 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

还有我的 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);
            }
        }
    }

在这个@PostConstruct 方法的最后,我想用这些设置创建一个BasicDataSource 并将它添加到ApplicationContext。但是,如果我尝试通过实现 BeanFactoryPostProcessor 并实现 postProcessBeanFactory 来实现此目的,则 clients 属性为空,我使用 @PostConstruct 填充的 CLIENT_DATASOURCES 也是如此。

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    System.out.println("clients: " + CLIENT_DATASOURCES);
}

使用 Spring Boot 即时创建数据源的最佳方式是什么?

【问题讨论】:

  • 这是一个老问题,所以值得old answer。它不是特定于 Spring Boot,而是通用于 Spring。我认为它仍然成立。

标签: java spring spring-boot


【解决方案1】:

创建 bean 并让 Spring Boot 向其中注入值怎么样?

类似

@Bean
@ConfigurationProperties("ds.client1")
public DataSource dataSourceClient1() {
    DataSourceBuilder.create().build();
}

@Bean
@ConfigurationProperties("ds.client2")
public DataSource dataSourceClient2() {
    DataSourceBuilder.create().build();
}

那么,ds.client1 命名空间中的任何设置都属于第一个数据源(即ds.client1.password 是该DataSource 的数据源密码)。

但也许您不知道您将拥有多少数据源?这变得越来越复杂,尤其是当您需要将这些动态数据源注入其他对象时。如果您只需要按名称查找它们,您可以自己将它们注册为单例。这是一个有效的例子

@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();
    }
}

请注意,这些 bean可通过 bean 名称查找获得。让我知道这是否适合你。

【讨论】:

  • 我认为第一个选项行不通,因为我不知道我将拥有多少数据源。
  • 好吧,那我再谈谈第二种情况。
  • 我猜这些数据源绑定到其他对象(即您需要将它们连接到其他 bean)?
  • 我要动态创建的数据源没有绑定到其他对象。它们被 Apache Camel 中的 SQL 语句引用。例如:sql:select * from table where id=# order by name?dataSource=myDS
  • 感谢 Stéphane - 成功了!我确实需要添加一个检查以查看 bean 是否已经注册(用于单元测试)。此外,我从 List 客户端更改为 List 以摆脱一些字符串解析。干杯!
【解决方案2】:

我在 github 上创建了一个示例项目来演示您的用例。

https://github.com/lhotari/dynamic-datasources

我实现了一个ImportBeanDefinitionRegistrar 来添加bean。您可以通过实现EnvironmentAware 来获取配置。可能还有其他方法可以实现您的目标,但这是我在GspAutoConfiguration 中用于动态注册 bean 的方式。 GspAutoConfiguration 使 Grails GSP 在 Spring Boot 应用程序中可用。

下面是动态数据源示例中的相关配置类: 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;
    }

}

【讨论】:

  • 这很酷,但是如何为 CrudRepository 和 JdbcTemplate 选择合适的 DS 呢?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-25
  • 1970-01-01
  • 2018-03-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多