Spring Boot change DataSource and JPA properties at runtime

10,828

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.

Share:
10,828
rvit34
Author by

rvit34

Updated on July 05, 2022

Comments

  • rvit34
    rvit34 almost 2 years

    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?