Spring Boot動態建立Bean,DynamicDataSource實現讀寫分離的多數據源

一. 從Spring 3.0開始,增長了一種新的途徑來配置Bean Definition,這就是經過Java Code配置Bean Definition。

與XML和Annotation兩種配置方式不一樣點在於:

   前兩種方式XML和Annotation的配置方式爲預約義方式,即開發人員經過XML文件或者Annotation預約義配置Bean的各類屬性後,啓動Spring容器,Spring容器會首先解析這些配置屬性,生成對應的Bean Definition,裝入到DefaultListtableBeanFactory對象的屬性容器中,以此同時,Spring框架也會定義內部使用的Bean定義,如Bean名爲:org.springframework.context.annotation.internalConfigurationAnnotationProcessor」的 ConfigurationClassPostProcessor 定義。然後此刻不會作任何Bean Definition的解析動做,Spring框架會根據前兩種配置,過濾出BeanDefinitionRegistryPostProcessor 類型的Bean定義,並經過Spring框架生成對應的Bean對象(如 ConfigurationClassPostProcessor 實例)。。結合 Spring 上下文源碼可知這個對象是一個 processor 類型工具類,Spring 容器會在實例化開發人員所定義的 Bean 前先調用該 processor 的 postProcessBeanDefinitionRegistry(…) 方法。此處實現基於 Java Code 配置Bean Definition的處理。



   基於 Java Code 的配置方式,其執行原理不一樣於前兩種。它是在 Spring 框架已經解析了基於 XML 和 Annotation 配置後,經過加入 BeanDefinitionRegistryPostProcessor 類型的 processor 來處理配置信息,讓開發人員經過 Java 編程方式定義一個 Java 對象。其優勢在於能夠將配置信息集中在必定數量的 Java 對象中,同時經過 Java 編程方式,比基於 Annotation 方式具備更高的靈活性。而且該配置方式給開發人員提供了一種很是好的範例來增長用戶自定義的解析工具類。其主要缺點在於與 Java 代碼結合緊密,配置信息的改變須要從新編譯 Java 代碼,另外這是一種新引入的解析方式,須要必定的學習成本。

二.說起一點的就是,Spring框架有3個主要的Hook類,分別是:

  1. org.springframework.context.ApplicationContextAware 它的setApplicationContext 方法將在Spring啓動以前第一個被調用。咱們用來同時啓動Jdon框架。css

  2. org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor 它的postProcessBeanDefinitionRegistry 和 postProcessBeanFactory 方法是第二和第三被調用,它們在Bean初始化建立以前啓動,若是Spring的bean須要的其餘第三方中的組件,咱們在這裏將其注入給Spring。java

  3. org.springframework.context.EnvironmentAware 這裏介紹凡是被spring管理的類,實現接口 EnvironmentAware 重寫方法 setEnvironment 能夠在工程啓動時,獲取到系統環境變量和application配置文件中的變量。mysql

  • @ConditionOnClass代表該@Configuration僅僅在必定條件下才會被加載,這裏的條件是 DataDruidConfig.class位於類路徑上web

  • @EnableConfigurationProperties將Spring Boot的配置文件(application.properties)中的spring.datasource.druid.*屬性映射爲DataDruidProperties並注入到 DataDruidConfig中。spring

  • @ConditionalOnMissingBean說明Spring Boot僅僅在當前上下文中不存在DataDruidConfig對象時,纔會實例化一個Bean。這個邏輯也體現了Spring Boot的另一個特性——自定義的Bean優先於框架的默認配置,咱們若是顯式的在業務代碼中定義了一個DataDruidConfig對象,那麼Spring Boot就再也不建立。sql

  1. org.springframework.context.ApplicationListener 用於在初始化完成後作一些事情,當Spring全部XML或元註解的Bean都啓動被建立成功了,這時會調用它的惟一方法onApplicationEvent。

三.數據庫參數配置管理實現類,基於DataSource Druid 實現,具體屬於配置以下:

package top.suven.core.db;



/**
* DruidDataSource 數據庫鏈接管理對象的相關參數屬性配置對象;
* 數據庫鏈接池的相結合配置文件類型定義參數;
*/

public interface DataDruidConfig {


   /**
    * DruidDataSource 對應屬性的配置文件參數;可對應變動和修改
    * 根據本身的項目配置文件規則,只須要調整***PREFIX對應的參數便可;
    */
     String DATASOURCE_DRUID_PREFIX = "suven.datasource.druid.";

   /**
    * 數據庫對象的模塊的url,username,password的前綴
    * 根據本身的項目配置文件規則,只須要調整***PREFIX對應的參數便可;
    * suven.datasource.%s.%s.url        //eg: suven.datasource.assets.master.url
    * suven.datasource.%s.%s.username   //eg: suven.datasource.assets.master.username
    * suven.datasource.%s.%s.password;  //eg: suven.datasource.assets.master.password
    */
     String DATASOURCE_MODULE_PREFIX = "suven.datasource.";
     String DATASOURCDE_DRUID_FORMAT = "%s.%s.";



   /**
    * 經過 BeanDefinitionBuilder類,初始化DruidDataSource對應的屬性參考
    */


     String URL                                        = "url";
     String USERNAME                                   = "username";
     String PASSWORD                                  = "password";

     String DRIVER_CLASSNAME                            = "driverClassName";
     String INITIALIZE                                = "initialize";
     String DBTYPE                                     = "dbType";
     String MAXACTIVE                                = "maxActive";
     String INITIALSIZE                              = "initialSize";
     String MAXWAIT                                   = "maxWait";
     String MINIDLE                                   = "minIdle";
     String TIME_BETWEENE_VICTION_RUNS_MILLIS             = "timeBetweenEvictionRunsMillis";
     String MIN_EVICTABLE_IDLE_TIME_MILLIS                 = "minEvictableIdleTimeMillis";
     String VALIDATION_QUERY                           = "validationQuery";
     String TEST_WHILEIDLE                            = "testWhileIdle";
     String TEST_ON_BORROW                             = "testOnBorrow";
     String  TEST_ON_RETURN                             = "testOnReturn";
     String POOL_PREPARED_STATEMENTS                      = "poolPreparedStatements";
     String CONNECTION_PROPERTIES                        =    "connectionProperties";
     String FILTERS                                     = "filters";

     String ENABLED                                     = "enabled";

     String datasource_druid_master                    = "master";
     String datasource_druid_slave                     ="slave";
	  String datasource_druid_frame                     ="druid";
     String datasource_master_name                     = "MasterDataSource";
     String datasource_slave_name                      =  "SlaveDataSource";

     String datasource_param_config_enabled            = "config.enabled";


   /**
    * 初化全部數據源DataSourceAutoConfiguration類配置開關,默認爲falase
    * suven.datasource.druid.frame.enabled=true
    */
     String datasource_druid_config_enabled                     =   DATASOURCE_DRUID_PREFIX +  datasource_param_config_enabled ;//"suven.datasource.druid.frame.enabled";


   /**
    * 初化指定模塊數據源DataSourceGroupNameEnum類配置開關,默認爲 true
    * suven.datasource.user.master.enabled=true
    */
     String datasource_druid_master_enabled  =                    DATASOURCE_MODULE_PREFIX +  DATASOURCDE_DRUID_FORMAT + ENABLED;//   "suven.datasource.%s.%s.enabled";

   /**
    * 初化全部數據源DataSourceAutoConfiguration類配置 從數據庫的總開關,
    * 對應數據庫的從數據庫集合開關;默認爲 true,
    * 若存在對應的從數據庫的配置會自動加載,若須要關閉能夠將該配置設計爲false,或刪除對應配置
    * suven.datasource.druid.user.slave.enabled=true
    */
     String datasource_druid_slave_enabled =                           DATASOURCE_MODULE_PREFIX + DATASOURCDE_DRUID_FORMAT + ENABLED;//    "suven.datasource.druid.%s.slave.enabled";

     String datasource_druid_url =                                        DATASOURCE_MODULE_PREFIX + DATASOURCDE_DRUID_FORMAT + URL;//    "suven.datasource.%s.%s.url";// = jdbc:mysql://127.0.0.1:3306/datasource_name?autoReconnect=true&characterEncoding=utf-8
     String datasource_druid_username =                                   DATASOURCE_MODULE_PREFIX +  DATASOURCDE_DRUID_FORMAT + USERNAME;//   "suven.datasource.%s.%s.username";//suven.datasource.assets.master.username = redfinger
     String datasource_druid_password =                                   DATASOURCE_MODULE_PREFIX + DATASOURCDE_DRUID_FORMAT +  PASSWORD;//    "suven.datasource.%s.%s.password";// suven.datasource.assets.master.password = redfinger





     String datasource_druid_initialSize =                            DATASOURCE_DRUID_PREFIX + INITIALSIZE;// "suven.datasource.druid.initialSize";
     String datasource_druid_minIdle =                                 DATASOURCE_DRUID_PREFIX + MINIDLE;//"suven.datasource.druid.minIdle";
     String datasource_druid_maxActive =                              DATASOURCE_DRUID_PREFIX + MAXACTIVE;// "suven.datasource.druid.maxActive";
     String datasource_druid_maxWait =                                DATASOURCE_DRUID_PREFIX + MAXWAIT;// "suven.datasource.druid.maxWait";
     String datasource_druid_timeBetweenEvictionRunsMillis =          DATASOURCE_DRUID_PREFIX + TIME_BETWEENE_VICTION_RUNS_MILLIS;//  "suven.datasource.druid.timeBetweenEvictionRunsMillis";
     String datasource_druid_minEvictableIdleTimeMillis =              DATASOURCE_DRUID_PREFIX + MIN_EVICTABLE_IDLE_TIME_MILLIS;// "suven.datasource.druid.minEvictableIdleTimeMillis";
     String datasource_druid_validationQuery =                         DATASOURCE_DRUID_PREFIX + VALIDATION_QUERY;// "suven.datasource.druid.validationQuery";
     String datasource_druid_testWhileIdle =                              DATASOURCE_DRUID_PREFIX + TEST_WHILEIDLE;//  "suven.datasource.druid.testWhileIdle";
     String datasource_druid_testOnBorrow =                              DATASOURCE_DRUID_PREFIX + TEST_ON_BORROW;//  "suven.datasource.druid.testOnBorrow";
     String datasource_druid_testOnReturn =                             DATASOURCE_DRUID_PREFIX + TEST_ON_RETURN;//    "suven.datasource.druid.testOnReturn";
     String datasource_druid_poolPreparedStatements =                   DATASOURCE_DRUID_PREFIX + POOL_PREPARED_STATEMENTS;//    "suven.datasource.druid.poolPreparedStatements";
     String datasource_druid_filters =                                   DATASOURCE_DRUID_PREFIX + FILTERS;//    "suven.datasource.druid.filters";
     String datasource_druid_connectionProperties =                       DATASOURCE_DRUID_PREFIX + CONNECTION_PROPERTIES;//    "suven.datasource.druid.connectionProperties";

}

四.繼承AbstractRoutingDataSource數據源多數據庫動態路由切換,將 DataSourceGroupNameEnum 數據源組 生成bean對象注入到spring管理容器中;再根據模塊名稱字符串 做爲模塊key 從targetDataSources 的Map中獲取指定模塊的數據庫的鏈接池;從而經過動態橋接的設計模式來達到數據源鏈接池動態切換原理;

package top.suven.core.db;

import com.alibaba.druid.pool.DruidDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * 獲取數據源
 * 
 * @author suven
 * @version 2018/6/15 13:18
 */

/**
 * 將 DataSourceGroupNameEnum 數據源組 生成bean對象注入到spring管理容器中;
 * 經過 datasource_druid_enabled 對應模塊數據庫配置總開關,默認值爲true
 * 經過 datasource_druid_slave_enabled 對應模塊從數據庫配置總開關,默認值爲true
 * 將模塊初始化後數據源信息,並將結果生成spring bean 名稱緩存到DataSourceGroupNameEnum對象中
 * 將全部模塊經過initDataByGroup 初始化後的數據庫的聚羣,初始化到目標的動態數據池子裏;
 * 再根據模塊名稱字符串 做爲模塊key 從targetDataSources 的Map中獲取指定模塊的數據庫的鏈接池;
 * 從而經過動態橋接的設計模式來達到數據源鏈接池動態切換原理;
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

	private final Logger logger = LoggerFactory.getLogger(getClass());

	private boolean isDefaultTargetDataSource = true;

	private Map<Object, Object> targetDataSources = new HashMap<>();
	private ConcurrentHashMap<String,AtomicInteger> currentMap = new ConcurrentHashMap<>();


	/**
	 * 將全部模塊經過initDataByGroup 初始化後的數據庫的聚羣,初始化到目標的動態數據池子裏;
	 */
	public void setTargetDataSources(){
		super.setTargetDataSources(targetDataSources);
	}


	public void setTargetDataSources(Map<Object, Object> targetDataSources){
		Object defaultTargetDataSource = targetDataSources.entrySet().iterator().next();
		if(isDefaultTargetDataSource){
			this.setDefaultTargetDataSource(defaultTargetDataSource);
			isDefaultTargetDataSource =false;
		}
		this.targetDataSources.putAll(targetDataSources);
		this.setTargetDataSources();
	}



	/**
	 * DataSourceGroupNameEnum 枚舉類型的實現初始化指定模塊數據源信息;
	 * @param dataSourceGroupNameEnum
	 */
	public void initDataByGroup(DataSourceGroupNameEnum dataSourceGroupNameEnum,ApplicationContext applicationContext){
		Map<Object, Object> targetDataSources = new HashMap<>();

		DataSourceGroup group = DataSourceGroupNameEnum.getDataSourceGroupByName(dataSourceGroupNameEnum);
		if(null == group || null == group.getMasterSources()){
			return;
		}
		String master = group.getMasterSources();
		DataSource defaultTargetDataSource = applicationContext.getBean(master,DruidDataSource.class);
		targetDataSources.put(master, defaultTargetDataSource);
		List<String> list = group.getSlaveSources();
		if(null != list && !list.isEmpty()){
			for (String slave : list){
				try{
					DataSource datasource = applicationContext.getBean(slave,DruidDataSource.class);
					if(null == datasource){
						continue;
					}
					targetDataSources.put(slave,datasource );
				}catch (Exception e){
					logger.info("初始化[{}】 slave 數據源失敗,檢查slave開關是否開啓!", slave);
				}
			}
		}
		if(isDefaultTargetDataSource){
			this.setDefaultTargetDataSource(defaultTargetDataSource);
		}

		this.targetDataSources.putAll(targetDataSources);

	}

	// 獲取數據源名稱,採用輪詢的方式實現
	@Override
	protected Object determineCurrentLookupKey() {
		DataChooseParam dataSource = DataSourceHolder.getDataSource();
		DataSourceHolder.clear();
		if (null == dataSource) {
			throw new IllegalArgumentException("Property 'determineCurrentLookupKey' is DataSourceHolder.getDataSource() not  dataSource:[" + dataSource + "]");

		}
		DataSourceGroup dataSourceGroup = DataSourceGroupNameEnum.getDataSourceGroupByName(dataSource.getGroupName());

		if (null == dataSourceGroup) {
			throw new IllegalArgumentException("Property 'determineCurrentLookupKey' is DataSourceGroupNameEnum.getDataSourceGroupByName() not  dataSourceGroup:[" + dataSourceGroup + "]");
		}
		String dataSourceKey = null;
//		Map<String,List<String>>  map = dataSourceGroupMap.get(dataSource.getGroupName());
		DataSourceEnum dataEnum = dataSource.getDataType();
		if (null == dataEnum) {
			dataEnum = DataSourceEnum.MASTER;
		}

		if (DataSourceEnum.MASTER == dataSource.getDataType()) {
			dataSourceKey = dataSourceGroup.getMasterSources();
			dataSource.setDataClient(dataSourceKey);
			return dataSourceKey;
		}
		if (DataSourceEnum.SLAVE == dataSource.getDataType()) {
			List<String> list = dataSourceGroup.getSlaveSources();
			if (null == list || list.isEmpty()) {
				dataSourceKey = dataSourceGroup.getMasterSources();
				dataSource.setDataClient(dataSourceKey);
				return dataSourceKey;
			}
//			throw new IllegalArgumentException("Property 'determineCurrentLookupKey' is DataSourceHolder.map.get(dataType) list isEmpty or null ");
			int size = list.size();
			if (size == 1) {
				dataSourceKey = list.get(0);
			} else {
				AtomicInteger counter = currentMap.get(dataEnum.name());
				if (counter == null) {
					counter = new AtomicInteger(0);
					currentMap.put(dataEnum.name(), counter);
				} else {
					if (counter.incrementAndGet() >= size) {
						counter.set(0);
					}
				}
				dataSourceKey = list.get(counter.intValue());
			}
		}
		dataSource.setDataClient(dataSourceKey);
		return dataSourceKey;

	}

五.結合 Spring 上下文源碼可知這個對象是一個 processor 類型工具類,Spring 容器會在實例化開發人員所定義的 Bean 前先調用該 processor 的 postProcessBeanDefinitionRegistry(…) 方法,下面咱們來完成一個,本身經過java代碼建立bean,並註冊爲Spring管理。

本例中,咱們建立一個接口,而後建立該接口的2個實現類,分別命名不一樣的名字,而後在須要注入的地方使用@Qualifier 指定注入對應的實例。數據庫

package top.suven.core.db;

import com.alibaba.druid.pool.DruidAbstractDataSource;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.*;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;

import javax.sql.DataSource;
import java.util.*;

/**
 * 應用服務起動類,加載數據源實現模塊組實現類
 * 數據庫聚羣,啓動和管理的配置實現類;
 * datasource_druid_config_enabled =true.表示啓動該實現類來初始化DataSourceGroupNameEnum對象管理的數據源,
 * 默認值爲false,
 * 數據庫的初始參數都是統一的,實現類爲DruidDataConfig,統一從Environment environment 初始化獲取;
 *
 * setEnvironment()-->postProcessBeanDefinitionRegistry() --> postProcessBeanFactory()
 */

@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class,
        DruidDataSourceAutoConfigure.class,JdbcTemplateAutoConfiguration.class})
@ConditionalOnProperty(name = DataDruidConfig.datasource_druid_config_enabled,  matchIfMissing = false)
public class DataSourceAutoConfig implements DataDruidConfig, EnvironmentAware, BeanDefinitionRegistryPostProcessor,ApplicationContextAware {

    private final Logger logger = LoggerFactory.getLogger(DataSourceAutoConfig.class);

    private RelaxedPropertyResolver property;
    private DruidDataConfig druidConfig;

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.property = new RelaxedPropertyResolver(environment);
        druidConfig = new DruidDataConfig();
        logger.warn("DataSourceAutoConfiguration in DruidDataConfig info=[{}]", druidConfig.toString());

    }


    @Bean("dataSource")
    public DataSource routingDataSource() {
        DynamicDataSource dataSource = new DynamicDataSource();
        Set<DataSourceGroupNameEnum> sourceNames = DataSourceGroupNameEnum.getSourceNames();
        if (sourceNames == null || sourceNames.isEmpty()) {
            throw  new RuntimeException("DynamicDataSource init DataSource isEmpty ");
        }
        for (DataSourceGroupNameEnum dataName : sourceNames) {
            dataSource.initDataByGroup(dataName,applicationContext);
        }
        dataSource.setTargetDataSources();

        logger.warn("Dynamic DataSource Registry --- routingDataSource Successfully ...      ");
        return dataSource;
    }



    /**
     * 將 DataSourceGroupNameEnum 數據源組 生成bean對象注入到spring管理容器中;
     * 經過 datasource_druid_enabled 對應模塊數據庫配置總開關,默認值爲true
     * 經過 datasource_druid_slave_enabled 對應模塊從數據庫配置總開關,默認值爲true
     * 將模塊初始化後數據源信息,並將結果生成spring bean 名稱緩存到DataSourceGroupNameEnum對象中
     * @param registry
     * @throws BeansException
     */
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

        Set<DataSourceGroupNameEnum> sourceNames = null;
        sourceNames = DataSourceGroupNameEnum.getSourceNames();
        if (sourceNames == null || sourceNames.isEmpty()) {
            return;
        }
        boolean isPrimary = true;
        //從數據庫配置總開關,
         boolean dataSourceSlaveEnabled = property.getProperty(
                String.format(datasource_druid_slave_enabled,datasource_druid_frame,datasource_druid_slave).trim(),
                Boolean.class,true);

        for (DataSourceGroupNameEnum dataName : sourceNames) {

            //模塊數據庫 主-從 配置總開關,
            boolean moduleDataSourceEnabled = property.getProperty(
                    String.format(datasource_druid_master_enabled,dataName.getValue(),datasource_druid_master).trim(),
                    Boolean.class,true);

            //模塊從數據庫配置總開關,
            boolean moduleDataSourceSlaveEnabled = property.getProperty(
                    String.format(datasource_druid_slave_enabled,dataName.getValue(),datasource_druid_slave).trim(),
                    Boolean.class,true);

            /**
             * 經過模塊對應的配置文件獲取主數據庫信息,若是不存在就跳該模塊的對應的全部數據庫
             */
            String url = property.getProperty(String.format(datasource_druid_url,dataName.getValue(),datasource_druid_master));
            if (null == url || "".equals(url) || !moduleDataSourceEnabled) {
                continue;
            }
            String username = property.getProperty(String.format(datasource_druid_username,dataName.getValue(),datasource_druid_master).trim());
            String password = property.getProperty(String.format(datasource_druid_password,dataName.getValue(),datasource_druid_master).trim());

            /**
             * 注入到spring bean的名稱生成規則;(模塊文稱+ MasterDataSource)
             */
            String datasourceMasterBeanName = dataName.getValue() + datasource_master_name ;

            BeanDefinitionBuilder datasourceFactory = initDatasourceBean(druidConfig,url,username,password);
            BeanDefinition beanDefinition =  datasourceFactory.getBeanDefinition();
            if(isPrimary){//設置惟一主數據庫
                beanDefinition.setPrimary(true);
                isPrimary = false;
            }
            registry.registerBeanDefinition(datasourceMasterBeanName, beanDefinition);
            List<String> slaveDataSources = new ArrayList<>();
            int i = 0 ;
            while (dataSourceSlaveEnabled && moduleDataSourceSlaveEnabled){

                String slave = i == 0 ? datasource_druid_slave : datasource_druid_slave + i;

                /**
                 * 注入到spring bean的名稱生成規則;(模塊文稱+ SlaveDataSource + 序列號1,2,3...)
                 */
                String datasourceSlaveBeanName = dataName.getValue() + datasource_slave_name + i;
                url = property.getProperty(String.format(datasource_druid_url,dataName.getValue(),slave).trim());
                if (null == url || "".equals(url)) {
                    break;
                }
                username = property.getProperty(String.format(datasource_druid_username,dataName.getValue(),slave).trim());
                password = property.getProperty(String.format(datasource_druid_password,dataName.getValue(),slave).trim());
                datasourceFactory = initDatasourceBean(druidConfig,url,username,password);
                registry.registerBeanDefinition(datasourceSlaveBeanName, datasourceFactory.getBeanDefinition());
                slaveDataSources.add(datasourceSlaveBeanName);
                i++;

            }
            /**
             * 將模塊初始化後數據源信息,並將結果生成spring bean 名稱緩存到DataSourceGroupNameEnum 對象中
             */
            DataSourceGroupNameEnum.setDataSource(dataName,datasourceMasterBeanName,slaveDataSources);

            logger.warn("DataSourceAutoConfig postProcessBeanDefinitionRegistry Registry --- dataSourceName[{}] Successfully ...",datasourceMasterBeanName);

        }

    }




    /**
     * 初始化DruidDataSource對象
     * 經過BeanDefinitionBuilder生成DruidDataSource對象實現類
     * 而且經過配置文件獲取對應的指定屬性
     * @param url
     * @param username
     * @param password
     * @return
     */
    private BeanDefinitionBuilder initDatasourceBean(DruidDataConfig druid,String url,String username,String password){
        BeanDefinitionBuilder datasourceFactory = BeanDefinitionBuilder.genericBeanDefinition(DruidDataSource.class);
        datasourceFactory.setLazyInit(true);          //設置是否懶加載
        datasourceFactory.setScope(BeanDefinition.SCOPE_SINGLETON);       //設置scope,爲單例類型
        datasourceFactory.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME);  //設置是否能夠被其餘對象自動注入

        datasourceFactory.addPropertyValue(URL, url);
        datasourceFactory.addPropertyValue(USERNAME, username);
        datasourceFactory.addPropertyValue(PASSWORD, password);

        initDataSource(datasourceFactory,druid);
        return datasourceFactory;
    }


//

    /**
     * 初始化數據庫的默認配置參數;
     * @param datasourceFactory
     */
    private void initDataSource(BeanDefinitionBuilder datasourceFactory,DruidDataConfig druid){

        datasourceFactory.addPropertyValue(INITIALSIZE, druid.getInitialSize());
        datasourceFactory.addPropertyValue(MINIDLE, druid.getMinIdle());
        datasourceFactory.addPropertyValue(MAXACTIVE, druid.getMaxActive());
        datasourceFactory.addPropertyValue(MAXWAIT, druid.getMaxWait());
        datasourceFactory.addPropertyValue(TIME_BETWEENE_VICTION_RUNS_MILLIS,druid.getTimeBetweenEvictionRunsMillis() );
        datasourceFactory.addPropertyValue(MIN_EVICTABLE_IDLE_TIME_MILLIS, druid.getMinEvictableIdleTimeMillis() );
        datasourceFactory.addPropertyValue(VALIDATION_QUERY, druid.getValidationQuery());
        datasourceFactory.addPropertyValue(TEST_WHILEIDLE, druid.isTestWhileIdle());
        datasourceFactory.addPropertyValue(TEST_ON_BORROW, druid.isTestOnBorrow());
        datasourceFactory.addPropertyValue(TEST_ON_RETURN, druid.isTestOnReturn());
        datasourceFactory.addPropertyValue(POOL_PREPARED_STATEMENTS, druid.isPoolPreparedStatements());

        try {
            datasourceFactory.addPropertyValue(FILTERS,druid.getFilters());
        } catch (Exception e) {
            logger.error("druid configuration initialization filter", e);
        }
    }
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    }


    private class DruidDataConfig {

        private int initialSize;
        private int minIdle;
        private int maxActive;
        private int maxWait;
        private long timeBetweenEvictionRunsMillis;
        private long minEvictableIdleTimeMillis;
        private String validationQuery;
        private boolean testWhileIdle;
        private boolean testOnBorrow;
        private boolean testOnReturn;
        private boolean poolPreparedStatements;
        private String filters;


        public DruidDataConfig(){
            this.setInitialSize(property.getProperty(datasource_druid_initialSize,Integer.class,DruidAbstractDataSource.DEFAULT_INITIAL_SIZE));
            this.setMinIdle(property.getProperty(datasource_druid_minIdle,Integer.class,DruidAbstractDataSource.DEFAULT_MIN_IDLE));
            this.setMaxActive(property.getProperty(datasource_druid_maxActive,Integer.class,DruidAbstractDataSource.DEFAULT_MAX_ACTIVE_SIZE));
            this.setMaxWait(property.getProperty(datasource_druid_maxWait,Integer.class,DruidAbstractDataSource.DEFAULT_MAX_WAIT));
            this.setTimeBetweenEvictionRunsMillis(property.getProperty(datasource_druid_timeBetweenEvictionRunsMillis,Long.class,DruidAbstractDataSource.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS));
            this.setMinEvictableIdleTimeMillis(property.getProperty(datasource_druid_minEvictableIdleTimeMillis,Long.class,DruidAbstractDataSource.DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS));
            this.setValidationQuery(property.getProperty(datasource_druid_validationQuery,DruidAbstractDataSource.DEFAULT_VALIDATION_QUERY));
            this.setTestWhileIdle(property.getProperty(datasource_druid_testWhileIdle,Boolean.class,true));
            this.setTestOnBorrow(property.getProperty(datasource_druid_testOnBorrow,Boolean.class));
            this.setTestOnReturn(property.getProperty(datasource_druid_testOnReturn,Boolean.class));
            this.setPoolPreparedStatements(property.getProperty(datasource_druid_poolPreparedStatements,Boolean.class));
            this.setFilters(property.getProperty(datasource_druid_filters,""));
        }

        public int getInitialSize() {
            return initialSize;
        }

        public void setInitialSize(int initialSize) {
            this.initialSize = initialSize;
        }

        public int getMinIdle() {
            return minIdle;
        }

        public void setMinIdle(int minIdle) {
            this.minIdle = minIdle;
        }

        public int getMaxActive() {
            return maxActive;
        }

        public void setMaxActive(int maxActive) {
            this.maxActive = maxActive;
        }

        public int getMaxWait() {
            return maxWait;
        }

        public void setMaxWait(int maxWait) {
            this.maxWait = maxWait;
        }

        public long getTimeBetweenEvictionRunsMillis() {
            return timeBetweenEvictionRunsMillis;
        }

        public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) {
            this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
        }

        public long getMinEvictableIdleTimeMillis() {
            return minEvictableIdleTimeMillis;
        }

        public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) {
            this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
        }

        public String getValidationQuery() {
            return validationQuery;
        }

        public void setValidationQuery(String validationQuery) {
            this.validationQuery = validationQuery;
        }

        public boolean isTestWhileIdle() {
            return testWhileIdle;
        }

        public void setTestWhileIdle(boolean testWhileIdle) {
            this.testWhileIdle = testWhileIdle;
        }

        public boolean isTestOnBorrow() {
            return testOnBorrow;
        }

        public void setTestOnBorrow(boolean testOnBorrow) {
            this.testOnBorrow = testOnBorrow;
        }

        public boolean isTestOnReturn() {
            return testOnReturn;
        }

        public void setTestOnReturn(boolean testOnReturn) {
            this.testOnReturn = testOnReturn;
        }

        public boolean isPoolPreparedStatements() {
            return poolPreparedStatements;
        }

        public void setPoolPreparedStatements(boolean poolPreparedStatements) {
            this.poolPreparedStatements = poolPreparedStatements;
        }

        public String getFilters() {
            return filters;
        }

        public void setFilters(String filters) {
            this.filters = filters;
        }

        @Override
        public String toString() {
            return "DruidDataConfig{" +
                    "initialSize=" + initialSize +
                    ", minIdle=" + minIdle +
                    ", maxActive=" + maxActive +
                    ", maxWait=" + maxWait +
                    ", timeBetweenEvictionRunsMillis=" + timeBetweenEvictionRunsMillis +
                    ", minEvictableIdleTimeMillis=" + minEvictableIdleTimeMillis +
                    ", validationQuery='" + validationQuery + '\'' +
                    ", testWhileIdle=" + testWhileIdle +
                    ", testOnBorrow=" + testOnBorrow +
                    ", testOnReturn=" + testOnReturn +
                    ", poolPreparedStatements=" + poolPreparedStatements +
                    ", filters='" + filters + '\'' +
                    '}';
        }
    }

}

六.數據源分組管理對象,主-從(多從)爲一組數據源,建立一個DataSourceGroup Bean 對象

package top.suven.core.db;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by suven on 16/8/30.
 * 封裝每一個項目的數據源對象;
 */
public class DataSourceGroup {

    private String groupName;
    private String masterSources;
    private List<String> slaveSources = new ArrayList<>();
    private String masterMethod;
    private String slaveMethod;
   


    public String getMasterSources() {
        return masterSources;
    }

    public void setMasterSources(String masterSources) {
        this.masterSources = masterSources;
    }

    public List<String> getSlaveSources() {
        return slaveSources;
    }

    public void setSlaveSources(List<String> slaveSources) {
        this.slaveSources = slaveSources;
    }

    public void setSlaveSources(String slaveKey) {
        if(this.slaveSources == null){
            slaveSources = new ArrayList<>();
        }
        this.slaveSources.add(slaveKey);
    }

    public String getMasterMethod() {
        return masterMethod;
    }

    public void setMasterMethod(String masterMethod) {
        this.masterMethod = masterMethod;
    }

    public String getSlaveMethod() {
        return slaveMethod;
    }

    public void setSlaveMethod(String slaveMethod) {
        this.slaveMethod = slaveMethod;
    }

    public String getGroupName() {
        return groupName;
    }

    public DataSourceGroup setGroupName(String groupName) {
        this.groupName = groupName;
        return this;
    }


}

 七.項目數據源名稱泛型實現類,數據庫組,包括主和從數據庫

package top.suven.core.db;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 項目數據源名稱泛型實現類,
 * 用來實現多數據源自動切換使用,配合 DataSourceAutoConfiguration 啓動初化數據源注入到spring bean 中
 * 建立spring DynamicDataSource bean 對象,並注入到spring容器中,命名爲dataSource;
 *
 *
 */
public enum DataSourceGroupNameEnum {

    DATA_NAME_USER("user"),//用戶組數據庫,包括主和從數據庫
    DATA_NAME_OAUTH("oauth"),//驗證組數據庫,包括主和從數據庫
    DATA_NAME_ASSETS("assets"),//用戶資產組數據庫,包括主和從數據庫
    //隨着項目和數據的增長多,在型這裏增長屬性便可
    ;

    public static Logger logger = LoggerFactory.getLogger(DataSourceGroupNameEnum.class);
    private String value;


    private static Map<String, DataSourceGroupNameEnum> tbTypeMap = new LinkedHashMap<>();
    private static Map<String,DataSourceGroup> groupMap = new ConcurrentHashMap<>();
    static {

        for(DataSourceGroupNameEnum type : values()) {
            tbTypeMap.put(type.name(), type);
            groupMap.put(type.name(), new DataSourceGroup().setGroupName(type.name()));
        }
    }



    DataSourceGroupNameEnum(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    private static DataSourceGroupNameEnum getName(String name){
        return tbTypeMap.get(name);
    }

    public static Collection<DataSourceGroupNameEnum> getValues(){
        return tbTypeMap.values();
    }
    public static Set<DataSourceGroupNameEnum> getSourceNames(){
        Set<DataSourceGroupNameEnum> sourceNames = new LinkedHashSet<>();
        sourceNames.addAll(tbTypeMap.values());
        return sourceNames;

    }


    //經過組名,獲取數據源集合對象;
    public static DataSourceGroup getDataSourceGroupByName(String groupName){
        if(null == groupName ){
            logger.warn("DataSourceGroup getDataSourceGroupByName by groupName[{}]", groupName);
            return null;
        }
        DataSourceGroupNameEnum dataSourceGroupNameEnum = getName(groupName);

        return getDataSourceGroupByName(dataSourceGroupNameEnum);
    }
    //經過組名,獲取數據源集合對象;
    public static DataSourceGroup getDataSourceGroupByName(DataSourceGroupNameEnum groupNameEnum){
        if( null == groupNameEnum){
            logger.warn("DataSourceGroup init getDataSourceGroupByName by groupNameEnum[{}]", groupNameEnum);
           return null;
        }
        DataSourceGroup dataSourceGroup = groupMap.get(groupNameEnum.name());
        return dataSourceGroup;
    }



    //將數據源對象添加到組名管理
    public static void setSlaveDataSourceKey(DataSourceGroupNameEnum groupNameEnum, String... datasourceKey){
        DataSourceGroup dataSourceGroup = groupMap.get(groupNameEnum.name());
        if( null == dataSourceGroup){
            logger.warn("DataSourceGroup init setMasterDataSourceKey by dataSourceGroup groupNameEnum[{}]", dataSourceGroup);
            return ;
        }
        if(datasourceKey.length == 1){
            dataSourceGroup.setSlaveSources(datasourceKey[0]);
        }else{
            dataSourceGroup.setSlaveSources(Arrays.asList(datasourceKey));
        }

    }

    public static void setDataSource(DataSourceGroupNameEnum groupNameEnum,String masterDatasourceKey, List<String> slaveDatasources ){
        DataSourceGroup dataSourceGroup = groupMap.get(groupNameEnum.name());
        if( null == dataSourceGroup){
            logger.warn("DataSourceGroup init setMasterDataSourceKey by dataSourceGroup groupNameEnum[{}]", dataSourceGroup);
            return ;
        }
        dataSourceGroup.setMasterSources(masterDatasourceKey);
        if(slaveDatasources != null){
            dataSourceGroup.setSlaveSources(slaveDatasources);
        }


    }
    //將數據源對象添加到組名管理
    public static void setDataSourceKey(DataSourceGroupNameEnum groupNameEnum,String masterDatasourceKey, String... slaveDatasourceKey){

        List<String> slaveList = new ArrayList();
        if( slaveDatasourceKey != null){
            slaveList = Arrays.asList(slaveDatasourceKey);
        }
        setDataSource(groupNameEnum,masterDatasourceKey,slaveList);
    }


}

八.經過 ThreadLocal 當前線程安全類,實現線程內,參數切換實現業務解偶; 以達到動態數據庫鏈接池實現切換,實現多數據源原理

package top.suven.core.db;

/**
 * Created by suven on 16/9/7.
 */
public class DataChooseParam {

    private boolean isRotate;
    private String groupName;
    private DataSourceEnum dataType; // 入參數爲: MASTER 或 SLAVE;
    private String dataClient;


    public DataChooseParam(){
        
    }
    public DataChooseParam(String groupName) {
        this.groupName = groupName;
        this.isRotate = true;
    }
    public DataChooseParam(String groupName, DataSourceEnum dataType) {
        this.groupName = groupName;
        this.dataType = dataType;
        this.isRotate = true;
    }

    public boolean isRotate() {
        return isRotate;
    }



    public String getGroupName() {
        return groupName;
    }



    public DataSourceEnum getDataType() {
        return dataType;
    }


    public String getDataClient() {
        return dataClient;
    }

    public void setDataClient(String dataClient) {
        this.dataClient = dataClient;
    }

    @Override
    public String toString() {
        return "DataChooseParam{" +
                "isRotate=" + isRotate +
                ", groupName='" + groupName + '\'' +
//                ", dataType='" + dataType + '\'' +
                ", dataClient='" + dataClient + '\'' +
                '}';
    }
}
  • DataSourceHolder 經過 ThreadLocal 當前線程安全類,實現線程內,參數切換實現業務解偶;以達到動態數據庫鏈接池實現切換,實現多數據源原理
package top.suven.core.db;


/**
 * @author suven.wang
 * @version
 *
 * 經過 ThreadLocal 當前線程安全類,實現線程內,參數切換實現業務解偶;
 * 以達到動態數據庫鏈接池實現切換,實現多數據源原理
 */
public class DataSourceHolder {
	// 數據源名稱線程池
	private static final ThreadLocal<DataChooseParam> holder = new ThreadLocal<>();
	



	public static void putDataSource(DataChooseParam dataChooseParam) {
		
		holder.set(dataChooseParam);
	}

	public static DataChooseParam getDataSource() {
		return holder.get();
	}

	public static void clear() {
		holder.remove();
	}
}
  • DataSourceEnum 數據類型,主-從
package top.suven.core.db;

public enum DataSourceEnum{
    MASTER, SLAVE;

}

九. 數據庫相關配置文件 application-db.properties 以下:

#-----------------------------------datasource--------------------------------------
# 是否啓動動態加載數據庫源,默認是false,不啓動該實現數據源
suven.datasource.druid.config.enabled=true

# 是否啓動動態加載全部從數據庫源,默認是true,支持動態讀寫分離
#suven.datasource.druid.slave.enabled=true

suven.datasource.druid.initialize=true 
suven.datasource.druid.dbType= postgresql
suven.datasource.druid.type = com.alibaba.druid.pool.DruidDataSource
suven.datasource.druid.driverClassName =org.postgresql.Driver
suven.datasource.druid.filters = stat
suven.datasource.druid.maxActive = 20
suven.datasource.druid.initialSize = 5
suven.datasource.druid.maxWait = 60000
suven.datasource.druid.minIdle = 5
suven.datasource.druid.timeBetweenEvictionRunsMillis = 60000
suven.datasource.druid.minEvictableIdleTimeMillis = 300000
suven.datasource.maxPoolPreparedStatementPerConnectionSize=1
suven.datasource.druid.validationQuery = select 'x';
suven.datasource.druid.testWhileIdle = true
suven.datasource.druid.testOnBorrow = false
suven.datasource.druid.testOnReturn = false
suven.datasource.druid.poolPreparedStatements = true
suven.datasource.druid.maxOpenPreparedStatements = 200

spring.datasource.druid.stat-view-servlet.enabled=false
spring.datasource.druid.statViewServlet.urlPattern=/druid/*
# \u767D\u540D\u5355\uFF1A
#spring.datasource.druid.statViewServlet.allow=
#  IP\u9ED1\u540D\u5355 (\u5B58\u5728\u5171\u540C\u65F6\uFF0Cdeny\u4F18\u5148\u4E8Eallow) : \u5982\u679C\u6EE1\u8DB3deny\u7684\u8BDD\u63D0\u793A:Sorry, you are not permitted to view this page.
#spring.datasource.druid.statViewServlet.deny=
spring.datasource.druid.statViewServlet.loginUsername=admin
spring.datasource.druid.statViewServlet.loginPassword=123456
# \u662F\u5426\u80FD\u591F\u91CD\u7F6E\u6570\u636E.
spring.datasource.druid.statViewServlet.resetEnable=false


spring.datasource.druid.web-stat-filter.enabled=false
spring.datasource.druid.webStatFilter.sessionStatEnable=false
spring.datasource.druid.webStatFilter.profileEnable=false
spring.datasource.druid.webStatFilter.urlPattern=/*
spring.datasource.druid.webStatFilter.exclusions="*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*,/server/druid/*
# \u914D\u7F6E\u65E5\u5FD7\u8F93\u51FA
spring.datasource.druid.filter.slf4j.enabled=false
spring.datasource.druid.filter.slf4j.statement-create-after-log-enabled=false
spring.datasource.druid.filter.slf4j.statement-close-after-log-enabled=false
spring.datasource.druid.filter.slf4j.result-set-open-after-log-enabled=false
spring.datasource.druid.filter.slf4j.result-set-close-after-log-enabled=false
# \u914D\u7F6E\u76D1\u63A7\u7EDF\u8BA1\u62E6\u622A\u7684filters\uFF0C\u53BB\u6389\u540E\u76D1\u63A7\u754C\u9762sql\u65E0\u6CD5\u7EDF\u8BA1\uFF0C'wall'\u7528\u4E8E\u9632\u706B\u5899
spring.datasource.filters=stat,wall,log4j
# \u901A\u8FC7connectProperties\u5C5E\u6027\u6765\u6253\u5F00mergeSql\u529F\u80FD\uFF1B\u6162SQL\u8BB0\u5F55
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# \u5408\u5E76\u591A\u4E2ADruidDataSource\u7684\u76D1\u63A7\u6570\u636E
spring.datasource.useGlobalDataSourceStat=true

spring.filter.dos.urlPatterns=/server/*
spring.filter.dos.exclusions=/druid/*,/server/druid/*



#-------------------------------------END--------------------------------------

# 根據項目須要增長數據源,減輕服務器數據庫壓力,只須要根據規則配置從數據源,並重啓項目便可...(,1,2),其它模塊也同樣
#---------------------------------ASSETS--------------------------------#
#suven.datasource.assets.master.enabled=false ## 關閉指定模塊的master主數據源,默認值爲true
#suven.datasource.assets.slave.enabled=false  ## 關閉指定模塊的全部slave從數據源,默認值爲true

#master
suven.datasource.assets.master.url = jdbc:mysql://127.0.0.1:3306/datasource_name?autoReconnect=true&characterEncoding=utf-8
suven.datasource.assets.master.username = test
suven.datasource.assets.master.password = test

#slave
suven.datasource.assets.slave.url = jdbc:mysql://127.0.0.1:3306/datasource_name?autoReconnect=true&characterEncoding=utf-8
suven.datasource.assets.slave.username = test
suven.datasource.assets.slave.password = test

#slave1
suven.datasource.assets.slave1.url = jdbc:mysql://127.0.0.1:3306/datasource_name?autoReconnect=true&characterEncoding=utf-8
suven.datasource.assets.slave1.username = test
suven.datasource.assets.slave1.password = test

#slave2
suven.datasource.assets.slave2.url = jdbc:mysql://127.0.0.1:3306/datasource_name?autoReconnect=true&characterEncoding=utf-8
suven.datasource.assets.slave2.username = test
suven.datasource.assets.slave2.password = test

#slave3
suven.datasource.assets.slave3.url = jdbc:mysql://127.0.0.1:3306/datasource_name?autoReconnect=true&characterEncoding=utf-8
suven.datasource.assets.slave3.username = test
suven.datasource.assets.slave3.password = test


#----------------------------------END----------------------------------#



#---------------------------------OAUTH--------------------------------#
#suven.datasource.oauth.master.enabled=false ## 關閉指定模塊的master主數據源,默認值爲true
#suven.datasource.oauth.slave.enabled=false  ## 關閉指定模塊的全部slave從數據源,默認值爲true
#master
suven.datasource.oauth.master.url = jdbc:mysql://127.0.0.1:3306/datasource_name?autoReconnect=true&characterEncoding=utf-8
suven.datasource.oauth.master.username = test
suven.datasource.oauth.master.password = test

#slave
suven.datasource.oauth.slave.url = jdbc:mysql://127.0.0.1:3306/datasource_name?autoReconnect=true&characterEncoding=utf-8
suven.datasource.oauth.slave.username = test
suven.datasource.oauth.slave.password = test

#slave1
suven.datasource.oauth.slave1.url = jdbc:mysql://127.0.0.1:3306/datasource_name?autoReconnect=true&characterEncoding=utf-8
suven.datasource.oauth.slave1.username = test
suven.datasource.oauth.slave1.password = test
#----------------------------------END-----------------------------------#





#---------------------------------USER--------------------------------#
#suven.datasource.user.master.enabled=false ## 關閉指定模塊的master主數據源,默認值爲true
#suven.datasource.user.slave.enabled=false  ## 關閉指定模塊的全部slave從數據源,默認值爲true
#master
suven.datasource.user.master.url = jdbc:mysql://127.0.0.1:3306/datasource_name?autoReconnect=true&characterEncoding=utf-8
suven.datasource.user.master.username = test
suven.datasource.user.master.password = test

#slave
suven.datasource.user.slave.url = jdbc:mysql://127.0.0.1:3306/datasource_name?autoReconnect=true&characterEncoding=utf-8
suven.datasource.user.slave.username = test
suven.datasource.user.slave.password = test

#slave1
suven.datasource.user.slave1.url = jdbc:mysql://127.0.0.1:3306/datasource_name?autoReconnect=true&characterEncoding=utf-8
suven.datasource.user.slave1.username = test
suven.datasource.user.slave1.password = test
#----------------------------------END----------------------------------#
相關文章
相關標籤/搜索