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?

rvit34
  • 1,967
  • 3
  • 17
  • 33
  • Possible duplicate of [dynamically change Spring data source](http://stackoverflow.com/questions/13507522/dynamically-change-spring-data-source) – Mario Santini Nov 03 '16 at 19:43
  • @Mario , It looks like a duplicate but answers there actually are not helpfull, If I understood correctly [AbstractRoutingDataSource](https://docs.spring.io/spring/docs/3.0.x/javadoc-api/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.html) 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. – rvit34 Nov 03 '16 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. – Mario Santini Nov 04 '16 at 08:50
  • Ok. Actually you were right. AbstractRoutingDataSource helped me. – rvit34 Dec 09 '16 at 13:39
  • Can you please share your solution? I’m kinda stuck with the same problem – jaletechs Aug 01 '19 at 22:19
  • @jaletechs see good explanation here - http://fedulov.website/2015/10/14/dynamic-datasource-routing-with-spring/ – rvit34 Aug 08 '19 at 20:24
  • @rvit34 Thanks for the link. I found a few tutorials similar to this, but realized I had a much bigger problem than I initially thought. I had 500 databases from a multitenant Java EE project and needed a way to dynamically connect to them without configuring datasources manually. I didn't find a way out though. – jaletechs Aug 11 '19 at 06:10
  • But.. this doesn't answer the actual question. The fix is not the same. The problem isn't that he has to tell which datasource they have to get. The problem is that the list of datasources is initialized at app startup and if runtime another datasource is added, the AbstractRoutingDataSource won't be requested again and repopulated with the new datasource.... That's what dynamic is.. no predefined datasources in the beginning but one and that's exactly the problem – metodski Nov 22 '19 at 08:23
  • @jaletechs I've added my solution. Sorry for late reply. – rvit34 Jan 30 '20 at 11:10
  • @metodski See my answer. This should help. Sorry for late reply. – rvit34 Jan 30 '20 at 11:11

1 Answers1

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.

rvit34
  • 1,967
  • 3
  • 17
  • 33