14

I am writing a desktop Spring Boot and Data-JPA application.
Initial settings come from application.properties (some spring.datasource.* and spring.jpa.*)
One of the features of my program is possibility to specify database settings (rdbms type,host,port,username,password and so on) via ui.
That's why I want to redefine already initialized db properties at runtime. That's why I am finding a way to do that.

I tried to do the following:
1) I wrote custom DbConfig where DataSource bean declared in Singleton Scope.

@Configuration public class DBConfig { @ConfigurationProperties(prefix = "spring.datasource") @Bean @Scope("singleton") @Primary public DataSource dataSource() { return DataSourceBuilder .create() .build(); } } 

2) In some DBSettingsController I got the instance of this bean and update new settings:

public class DBSettingsController { ... @Autowired DataSource dataSource; ... public void applySettings(){ if (dataSource instanceof org.apache.tomcat.jdbc.pool.DataSource){ org.apache.tomcat.jdbc.pool.DataSource tomcatDataSource = (org.apache.tomcat.jdbc.pool.DataSource) dataSource; PoolConfiguration poolProperties = tomcatDataSource.getPoolProperties(); poolProperties.setUrl("new url"); poolProperties.setDriverClassName("new driver class name"); poolProperties.setUsername("new username"); poolProperties.setPassword("new password"); } } } 

But it has no effect. Spring Data Repositories are steel using initialy initialized DataSource properties.

Also I heard about Spring Cloud Config and @RefreshScope. But i think it's a kind of overhead to run http webserver alongside of my small desktop application.

Might it is possible to write custom scope for such beans? Or by some way bind changes made in application.properties and corresponding beans properties?

10
  • Possible duplicate of dynamically change Spring data source Commented Nov 3, 2016 at 19:43
  • @Mario , It looks like a duplicate but answers there actually are not helpfull, If I understood correctly AbstractRoutingDataSource helps in case when there are two or more static predefined datasoures and need to switch beetween them at runtime. My case is a bit different. I have only one datasource and i need to change its properties at runtime. Besides that I need to change JPA properties as well. Commented Nov 3, 2016 at 20:17
  • 1
    your case is different, but the way to fix it is the same of the suggested answer. Your problem is that you have to tell to the beans which is the datasource they have to get, and the answer tell you a way to customize this behavior. Please take a look to the documentation of AbstractRoutingDataSource. Commented Nov 4, 2016 at 8:50
  • Ok. Actually you were right. AbstractRoutingDataSource helped me. Commented Dec 9, 2016 at 13:39
  • Can you please share your solution? I’m kinda stuck with the same problem Commented Aug 1, 2019 at 22:19

1 Answer 1

3

Here is my solution (it might be outdated as it was created in 2016th):

DbConfig (It does not really needed, I just added for completeness config)

import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.transaction.jta.JtaTransactionManager; import javax.sql.DataSource; @Configuration public class DBConfig extends HibernateJpaAutoConfiguration { @Value("${spring.jpa.orm}") private String orm; // this is need for my entities declared in orm.xml located in resources directory @SuppressWarnings("SpringJavaAutowiringInspection") public DBConfig(DataSource dataSource, JpaProperties jpaProperties, ObjectProvider<JtaTransactionManager> jtaTransactionManagerProvider) { super(dataSource, jpaProperties, jtaTransactionManagerProvider); } @Override @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory( EntityManagerFactoryBuilder factoryBuilder) { final LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = super.entityManagerFactory(factoryBuilder); entityManagerFactoryBean.setMappingResources(orm); return entityManagerFactoryBean; } } 

DataSourceConfig

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import javax.sql.DataSource; @Configuration public class DataSourceConfig { @Bean @Qualifier("default") @ConfigurationProperties(prefix = "spring.datasource") protected DataSource defaultDataSource(){ return DataSourceBuilder .create() .build(); } @Bean @Primary @Scope("singleton") public AbstractRoutingDataSource routingDataSource(@Autowired @Qualifier("default") DataSource defaultDataSource){ RoutingDataSource routingDataSource = new RoutingDataSource(); routingDataSource.addDataSource(RoutingDataSource.DEFAULT,defaultDataSource); routingDataSource.setDefaultTargetDataSource(defaultDataSource); return routingDataSource; } } 

My extension of RoutingDataSource:

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; public class RoutingDataSource extends AbstractRoutingDataSource { static final int DEFAULT = 0; static final int NEW = 1; private volatile int key = DEFAULT; void setKey(int key){ this.key = key; } private Map<Object,Object> dataSources = new HashMap(); RoutingDataSource() { setTargetDataSources(dataSources); } void addDataSource(int key, DataSource dataSource){ dataSources.put(new Integer(key),dataSource); } @Override protected Object determineCurrentLookupKey() { return new Integer(key); } @Override protected DataSource determineTargetDataSource() { return (DataSource) dataSources.get(key); } } 

And here it's special spring component to swith datasource in runtime:

import org.hibernate.boot.Metadata; import org.hibernate.boot.MetadataSources; import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.tool.hbm2ddl.SchemaUpdate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.stereotype.Component; import javax.sql.DataSource; @Component public class DBSettingsSwitcher { @Autowired private AbstractRoutingDataSource routingDataSource; @Value("${spring.jpa.orm}") private String ormMapping; public void applySettings(DBSettings dbSettings){ if (routingDataSource instanceof RoutingDataSource){ // by default Spring uses DataSource from apache tomcat DataSource dataSource = DataSourceBuilder .create() .username(dbSettings.getUserName()) .password(dbSettings.getPassword()) .url(dbSettings.JDBConnectionURL()) .driverClassName(dbSettings.driverClassName()) .build(); RoutingDataSource rds = (RoutingDataSource)routingDataSource; rds.addDataSource(RoutingDataSource.NEW,dataSource); rds.setKey(RoutingDataSource.NEW); updateDDL(dbSettings); } } private void updateDDL(DBSettings dbSettings){ /** worked on hibernate 5*/ StandardServiceRegistry registry = new StandardServiceRegistryBuilder() .applySetting("hibernate.connection.url", dbSettings.JDBConnectionURL()) .applySetting("hibernate.connection.username", dbSettings.getUserName()) .applySetting("hibernate.connection.password", dbSettings.getPassword()) .applySetting("hibernate.connection.driver_class", dbSettings.driverClassName()) .applySetting("hibernate.dialect", dbSettings.dialect()) .applySetting("show.sql", "false") .build(); Metadata metadata = new MetadataSources() .addResource(ormMapping) .addPackage("specify_here_your_package_with_entities") .getMetadataBuilder(registry) .build(); new SchemaUpdate((MetadataImplementor) metadata).execute(false,true); } } 

Where DB settings is just an interface (you should implement it according to your needs):

public interface DBSettings { int getPort(); String getServer(); String getSelectedDataBaseName(); String getPassword(); String getUserName(); String dbmsType(); String JDBConnectionURL(); String driverClassName(); String dialect(); } 

Having your own implementation of DBSettings and builded DBSettingsSwitcher in your Spring context, now you can just call DBSettingsSwitcher.applySettings(dbSettingsIml) and your data requests will be routed to new data source.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.