简体   繁体   中英

Dynamically create spring beans and alter properties on existing beans

I sucessfully managed to implement dynamic changing of database connections by following http://blog.springsource.com/2007/01/23/dynamic-datasource-routing/ article.

But now the problem is, I have a list of database urls in a configuration file that is managed by a legacy application.

Is there a way to create beans in that Spring context from a list of values (ie Year2011DataSource, Year2012DataSource,...) and populate map of the dataSource bean with those beans that were just created?

<!-- Property file located in the legacy application's folder -->
<context:property-placeholder location="file:///D:/config.properties" />

<!-- Shared data source properties are read from the config.properties file -->
<bean id="parentDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" abstract="true">
    <property name="driverClassName" value="${db.driver}" />
    <property name="username" value="${db.user}" />
    <property name="password" value="${db.password}" />
</bean>

<!-- Database urls by year -->
<bean id="Year2012DataSource" parent="parentDataSource">
    <property name="url" value="jdbc:sqlserver://localhost;databaseName=DbName_v570_2012" />
</bean>
<bean id="Year2011DataSource" parent="parentDataSource">
    <property name="url" value="jdbc:sqlserver://localhost;databaseName=DbName_v570_2011" />
</bean>
<bean id="Year2010DataSource" parent="parentDataSource">
    <property name="url" value="jdbc:sqlserver://localhost;databaseName=DbName_v570_2010" />
</bean>
<!-- ... and so on, these should instead be populated dynamically ... -->

<!-- DbConnectionRoutingDataSource extends AbstractRoutingDataSource -->
<bean id="dataSource" class="someProject.DbConnectionRoutingDataSource">
    <property name="targetDataSources">
        <map key-type="int">
            <entry key="2011" value-ref="Year2011DataSource" />
            <entry key="2010" value-ref="Year2010DataSource" />
            <!-- ... and so on, these also should instead be populated dynamically ... -->
        </map>
    </property>
    <property name="defaultTargetDataSource" ref="Year2012DataSource" />
</bean>

A good fit for this requirement I think is a custom BeanFactoryPostProcessor - read in the legacy configuration and generate the datasources in the custom bean factory post processor:

class MyDatasourceRegisteringBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        //Read in details from legacy properties.. build custom bean definitions and register with bean factory
        //for each legacy property...
            BeanDefinitionBuilder datasourceDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(BasicDataSource.class).addPropertyValue("url", "jdbc..");
            beanFactory.registerBeanDefinition(datasourceDefinitionBuilder.getBeanDefinition());
    }
}

I can tell you annotation approach. I would add urls and configuration in properties file and do something like following :

@Bean(name="dataSourceMap")
public Map<String, DataSource> dataSourceMap(DataSource dataSource2011, DataSource dataSource2012) {
    // read properties from properties file and create map of datasource

    Map<DataSource> map = new HashMap<>();
    map.put("dataSource2011",dataSource2011);
    map.put("dataSource2012",dataSource2012);
    //map.put("dataSource2013",dataSource2013);
    return map;
}

@Bean(name="dataSource2011",destroyMethod="close")
public DataSource dataSource() {
    // read properties from properties file and create map of 

    BasicDataSource dataSource = new BasicDataSource();
    dataSource.setDriverClassName(driverClassName);
    dataSource.setUrl(url2011);
    dataSource.setUsername(username2011);
    dataSource.setPassword(password2011);               
    return dataSource;
}

@Bean(name="dataSource2012",destroyMethod="close")
public DataSource dataSource() {
    // read properties from properties file and create map of 

    BasicDataSource dataSource = new BasicDataSource();
    dataSource.setDriverClassName(driverClassName);
    dataSource.setUrl(url2012);
    dataSource.setUsername(username2012);
    dataSource.setPassword(password2012);               
    return dataSource;
}

As far as I know, there is no out-of-the-box solution using XML configuration. However, a simple solution to achieve this is described in this answer using FactoryBean abstraction in Spring.

============================================

By following Biju's tip I've got everything working like this:

============================================

"Database urls by year" section in the spring configuration is no more, beans are created in the BeanFactoryPostProcessor.

"dataSource" bean has it's properties set to dummy data that is replaced in the BeanFactoryPostProcessor:

<bean id="dataSource" class="someProject.DbConnectionRoutingDataSource">
    <property name="targetDataSources">
        <map key-type="String">
            <!-- Will be filled from the DatasourceRegisteringBeanFactoryPostProcessor -->
        </map>
    </property>
    <property name="defaultTargetDataSource" value="java:jboss/datasources/ExampleDS" />
</bean>

And this is the BeanFactoryPostProcessor implementation:

@Component
class DatasourceRegisteringBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        InitialContext ic = new InitialContext();

        // read the list of available JNDI connections
        NamingEnumeration<?> list = ic.listBindings(getJndiDSRoot());
        HashSet<String> jndiDataSources = new HashSet<String>();
        while (list.hasMore()) {
            /*... (ommitted for simplicity) ...*/
            connectionsList.put(key, value);
        }            

        BeanDefinitionRegistry factory = (BeanDefinitionRegistry) beanFactory;
        BeanDefinitionBuilder datasourceDefinitionBuilder;

        // Create new beans
        for (Entry<Integer, String> e : connectionsList.entrySet()) {
            datasourceDefinitionBuilder = BeanDefinitionBuilder
                    .childBeanDefinition("parentDataSource")
                    .addPropertyValue("url", e.getValue());

            factory.registerBeanDefinition("Year" + e.getKey() + "DataSource",
                    datasourceDefinitionBuilder.getBeanDefinition());
        }

        // Configure the dataSource bean properties
        MutablePropertyValues mpv = factory.getBeanDefinition("dataSource").getPropertyValues();

        // Here you can set the default dataSource
        mpv.removePropertyValue("defaultTargetDataSource");
        mpv.addPropertyValue("defaultTargetDataSource", 
            new RuntimeBeanReference("Year" + defaultYear + "DataSource"));

        // Set the targetDataSource properties map with the list of connections
        ManagedMap<Integer, RuntimeBeanReference> mm = (ManagedMap<Integer, RuntimeBeanReference>) mpv.getPropertyValue("targetDataSources").getValue();
        mm.clear();

        // Fill the map with bean references to the newly created beans
        for (Entry<Integer, String> e : connectionsList.entrySet()) {
            mm.put(e.getKey(), new RuntimeBeanReference("Year" + e.getKey() + "DataSource")));
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM