简体   繁体   English

Spring Boot 在运行时更改 DataSource 和 JPA 属性

[英]Spring Boot change DataSource and JPA properties at runtime

I am writing a desktop Spring Boot and Data-JPA application.我正在编写桌面 Spring Boot 和 Data-JPA 应用程序。
Initial settings come from application.properties (some spring.datasource.* and spring.jpa.* )初始设置来自application.properties (一些spring.datasource.*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.我的程序的功能之一是可以通过 ui 指定数据库设置(rdbms 类型、主机、端口、用户名、密码等)。
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. 1)我编写了自定义 DbConfig,其中DataSource bean 在 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: 2)在某些 DBSettingsController 中,我获得了这个 bean 的实例并更新了新设置:

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. Spring Data Repositories 是使用初始初始化的 DataSource 属性的钢铁。

Also I heard about Spring Cloud Config and @RefreshScope .我也听说过Spring Cloud Config@RefreshScope But i think it's a kind of overhead to run http webserver alongside of my small desktop application.但我认为在我的小型桌面应用程序旁边运行 http webserver 是一种开销。

Might it is possible to write custom scope for such beans?是否可以为此类 bean 编写自定义范围? Or by some way bind changes made in application.properties and corresponding beans properties?或者通过某种方式绑定application.properties和相应的 bean 属性中所做的更改?

Here is my solution (it might be outdated as it was created in 2016th):这是我的解决方案(它可能已经过时,因为它是在 2016 年创建的):

DbConfig (It does not really needed, I just added for completeness config) DbConfig(它并不是真的需要,我只是为了完整性配置而添加的)

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:我的 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:这是在运行时切换数据源的特殊 spring 组件:

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):其中 DB 设置只是一个接口(您应该根据需要实现它):

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.拥有自己的 DBSettings 实现并在 Spring 上下文中构建 DBSettingsSwitcher,现在您只需调用DBSettingsSwitcher.applySettings(dbSettingsIml)并且您的数据请求将被路由到新的数据源。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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