JAVA中使用代碼建立多數據源,並實現動態切換(一)

 近日,博主有個業務需求,就是根據數據庫存儲的不一樣數據源信息,動態建立數據源並實現業務不一樣而轉到不一樣的數據源上處理。html

    數據庫存儲起來的數據源信息是不肯定的,能夠刪除和添加,這些是業務前提。java

    在網上找了下相關資料,對於使用Spring配置,直接配置多套數據源,使用AOP動態切換的方式居多,這種方式博主之前也使用過,很強大。不過有個前提就是多個數據源的信息是預先就肯定的。那麼對於不肯定數據源信息的業務需求,就只有使用代碼動態實現數據源初始化和銷燬操做了。mysql

    好了,有了這些思路,能夠開始準備寫代碼了。spring

一、建立一個線程上下文對象(使用ThreadLocal,保證線程安全)。上下文對象中主要維護了數據源的KEY和數據源的地址等信息,當KEY對應的數據源找不到時,根據數據源地址、驅動和用戶名等建立 一個數據源,這裏也是業務中須要解決的一個核心問題(JAVA動態建立數據源)。
 sql

/**
 * Copyright (c) 2015 - 2016 eay Inc.
 * All rights reserved.
 */
package com.eya.pubservice.datasource;

import java.util.HashMap;
import java.util.Map;

/**
 * 當前正在使用的數據源信息的線程上線文
 * @create ll
 * @createDate 2017年3月27日 下午2:37:07
 * @update 
 * @updateDate 
 */
public class DBContextHolder {
    /** 數據源的KEY */
    public static final String DATASOURCE_KEY = "DATASOURCE_KEY";
    /** 數據源的URL */
    public static final String DATASOURCE_URL = "DATASOURCE_URL";
    /** 數據源的驅動 */
    public static final String DATASOURCE_DRIVER = "DATASOURCE_DRIVER";
    /** 數據源的用戶名 */
    public static final String DATASOURCE_USERNAME = "DATASOURCE_USERNAME";
    /** 數據源的密碼 */
    public static final String DATASOURCE_PASSWORD = "DATASOURCE_PASSWORD";

    private static final ThreadLocal<Map<String, Object>> contextHolder = new ThreadLocal<Map<String, Object>>();

    public static void setDBType(Map<String, Object> dataSourceConfigMap) {
        contextHolder.set(dataSourceConfigMap);
    }

    public static Map<String, Object> getDBType() {
        Map<String, Object> dataSourceConfigMap = contextHolder.get();
        if (dataSourceConfigMap == null) {
            dataSourceConfigMap = new HashMap<String, Object>();
        }
        return dataSourceConfigMap;
    }

    public static void clearDBType() {
        contextHolder.remove();
    }

}

二、建立一個AbstractRoutingDataSource的子類,實現其determineCurrentLookupKey方法,用於決定使用哪個數據源。說明一下,這裏實現了ApplicationContextAware接口,用於在Spring加載完成後,注入Spring上下文對象,用於獲取Bean。數據庫

/**
 * Copyright (c) 2015 - 2016 eya Inc.
 * All rights reserved.
 */
package com.eya.pubservice.datasource;

import java.util.Map;

import javax.sql.DataSource;

import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 動態數據源父類
 * @create ll
 * @createDate 2017年3月27日 下午2:38:05
 * @update 
 * @updateDate 
 */
public abstract class AbstractDynamicDataSource<T extends DataSource> extends AbstractRoutingDataSource
                                                                                                implements
                                                                                                ApplicationContextAware {

    /** 日誌 */
    protected Logger logger = LoggerFactory.getLogger(getClass());
    /** 默認的數據源KEY */
    protected static final String DEFAULT_DATASOURCE_KEY = "defaultDataSource";

    /** 數據源KEY-VALUE鍵值對 */
    public Map<Object, Object> targetDataSources;

    /** spring容器上下文 */
    private static ApplicationContext ctx;

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ctx = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return ctx;
    }

    public static Object getBean(String name) {
        return ctx.getBean(name);
    }

    /**
     * @param targetDataSources the targetDataSources to set
     */
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
        super.setTargetDataSources(targetDataSources);
        // afterPropertiesSet()方法調用時用來將targetDataSources的屬性寫入resolvedDataSources中的
        super.afterPropertiesSet();
    }

    /**
     * 建立數據源
     * @param driverClassName 數據庫驅動名稱
     * @param url 鏈接地址
     * @param username 用戶名
     * @param password 密碼
     * @return 數據源{@link T}
     * @Author : ll. create at 2017年3月27日 下午2:44:34
     */
    public abstract T createDataSource(String driverClassName, String url, String username,
                                       String password);

    /**
     * 設置系統當前使用的數據源
     * <p>數據源爲空或者爲0時,自動切換至默認數據源,即在配置文件中定義的默認數據源
     * @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()
     */
    @Override
    protected Object determineCurrentLookupKey() {
        logger.info("【設置系統當前使用的數據源】");
        Map<String, Object> configMap = DBContextHolder.getDBType();
        logger.info("【當前數據源配置爲:{}】", configMap);
        if (MapUtils.isEmpty(configMap)) {
            // 使用默認數據源
            return DEFAULT_DATASOURCE_KEY;
        }
        // 判斷數據源是否須要初始化
        this.verifyAndInitDataSource();
        logger.info("【切換至數據源:{}】", configMap);
        return configMap.get(DBContextHolder.DATASOURCE_KEY);
    }

    /**
     * 判斷數據源是否須要初始化
     * @Author : ll. create at 2017年3月27日 下午3:57:43
     */
    private void verifyAndInitDataSource() {
        Map<String, Object> configMap = DBContextHolder.getDBType();
        Object obj = this.targetDataSources.get(configMap.get(DBContextHolder.DATASOURCE_KEY));
        if (obj != null) {
            return;
        }
        logger.info("【初始化數據源】");
        T datasource = this.createDataSource(configMap.get(DBContextHolder.DATASOURCE_DRIVER)
            .toString(), configMap.get(DBContextHolder.DATASOURCE_URL).toString(),
            configMap.get(DBContextHolder.DATASOURCE_USERNAME).toString(),
            configMap.get(DBContextHolder.DATASOURCE_PASSWORD).toString());
        this.addTargetDataSource(configMap.get(DBContextHolder.DATASOURCE_KEY).toString(),
            datasource);
    }

    /**
     * 往數據源key-value鍵值對集合添加新的數據源
     * @param key 新的數據源鍵
     * @param dataSource 新的數據源
     * @Author : ll. create at 2017年3月27日 下午2:56:49
     */
    private void addTargetDataSource(String key, T dataSource) {
        this.targetDataSources.put(key, dataSource);
        super.setTargetDataSources(this.targetDataSources);
        // afterPropertiesSet()方法調用時用來將targetDataSources的屬性寫入resolvedDataSources中的
        super.afterPropertiesSet();
    }

}

三、編寫AbstractDynamicDataSource的實現類,使用com.alibaba.druid.pool.DruidDataSource數據源。主要實現建立數據源的方法(createDataSource)apache

/**
 * Copyright (c) 2015 - 2016 eya Inc.
 * All rights reserved.
 */
package com.eya.pubservice.datasource;

import java.sql.SQLException;
import java.util.List;

import org.apache.commons.lang3.StringUtils;

import com.alibaba.druid.filter.Filter;
import com.alibaba.druid.pool.DruidDataSource;

/**
 * Druid數據源
 * <p>摘抄自http://www.68idc.cn/help/buildlang/java/20160606618505.html
 * @create ll
 * @createDate 2017年3月27日 下午2:40:17
 * @update 
 * @updateDate 
 */
public class DruidDynamicDataSource extends AbstractDynamicDataSource<DruidDataSource> {

    private boolean testWhileIdle = true;
    private boolean testOnBorrow = false;
    private boolean testOnReturn = false;

    // 是否打開鏈接泄露自動檢測
    private boolean removeAbandoned = false;
    // 鏈接長時間沒有使用,被認爲發生泄露時長
    private long removeAbandonedTimeoutMillis = 300 * 1000;
    // 發生泄露時是否須要輸出 log,建議在開啓鏈接泄露檢測時開啓,方便排錯
    private boolean logAbandoned = false;

    // 只要maxPoolPreparedStatementPerConnectionSize>0,poolPreparedStatements就會被自動設定爲true,使用oracle時能夠設定此值。
    //    private int maxPoolPreparedStatementPerConnectionSize = -1;

    // 配置監控統計攔截的filters
    private String filters; // 監控統計:"stat" 防SQL注入:"wall" 組合使用: "stat,wall"
    private List<Filter> filterList;

    /*
     * 建立數據源
     * @see com.cdelabcare.pubservice.datasource.IDynamicDataSource#createDataSource(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
     */
    @Override
    public DruidDataSource createDataSource(String driverClassName, String url, String username,
                                            String password) {
        DruidDataSource parent = (DruidDataSource) super.getApplicationContext().getBean(
            DEFAULT_DATASOURCE_KEY);
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl(url);
        ds.setUsername(username);
        ds.setPassword(password);
        ds.setDriverClassName(driverClassName);
        ds.setInitialSize(parent.getInitialSize());
        ds.setMinIdle(parent.getMinIdle());
        ds.setMaxActive(parent.getMaxActive());
        ds.setMaxWait(parent.getMaxWait());
        ds.setTimeBetweenConnectErrorMillis(parent.getTimeBetweenConnectErrorMillis());
        ds.setTimeBetweenEvictionRunsMillis(parent.getTimeBetweenEvictionRunsMillis());
        ds.setMinEvictableIdleTimeMillis(parent.getMinEvictableIdleTimeMillis());

        ds.setValidationQuery(parent.getValidationQuery());
        ds.setTestWhileIdle(testWhileIdle);
        ds.setTestOnBorrow(testOnBorrow);
        ds.setTestOnReturn(testOnReturn);

        ds.setRemoveAbandoned(removeAbandoned);
        ds.setRemoveAbandonedTimeoutMillis(removeAbandonedTimeoutMillis);
        ds.setLogAbandoned(logAbandoned);

        // 只要maxPoolPreparedStatementPerConnectionSize>0,poolPreparedStatements就會被自動設定爲true,參照druid的源碼
        ds.setMaxPoolPreparedStatementPerConnectionSize(parent
            .getMaxPoolPreparedStatementPerConnectionSize());

        if (StringUtils.isNotBlank(filters))
            try {
                ds.setFilters(filters);
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }

        addFilterList(ds);
        return ds;
    }

    private void addFilterList(DruidDataSource ds) {
        if (filterList != null) {
            List<Filter> targetList = ds.getProxyFilters();
            for (Filter add : filterList) {
                boolean found = false;
                for (Filter target : targetList) {
                    if (add.getClass().equals(target.getClass())) {
                        found = true;
                        break;
                    }
                }
                if (!found)
                    targetList.add(add);
            }
        }
    }
}

四、使用Spring配置默認數據源。系統運行確定有一套默認的數據源(不然動態建立的數據源信息從哪裏來呢?上面提到的,動態建立的數據源信息是存放在數據庫中的)。這裏我貼出完整的Spring配置。安全

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="
   http://www.springframework.org/schema/beans 
   http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
   http://www.springframework.org/schema/tx 
   http://www.springframework.org/schema/tx/spring-tx-4.2.xsd
   ">
	<bean id="defaultDataSource" class="com.alibaba.druid.pool.DruidDataSource"
		init-method="init" destroy-method="close">
		<!-- 基本屬性driverClassName、 url、user、password -->
		<property name="driverClassName" value="${pro.driver}" />
		<property name="url" value="${pro.url}" />
		<property name="username" value="${pro.username}" />
		<property name="password" value="${pro.password}" />

		<!-- 配置初始化大小、最小、最大 -->
		<property name="initialSize" value="${pro.initialSize}" />
		<property name="minIdle" value="${pro.minIdle}" />
		<property name="maxActive" value="${pro.maxActive}" />

		<!-- 配置獲取鏈接等待超時的時間 -->
		<property name="maxWait" value="${pro.maxWait}" />

		<!-- 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 -->
		<property name="timeBetweenEvictionRunsMillis" value="${pro.timeBetweenEvictionRunsMillis}" />

		<!-- 配置一個鏈接在池中最小生存的時間,單位是毫秒 -->
		<property name="minEvictableIdleTimeMillis" value="${pro.minEvictableIdleTimeMillis}" />

		<property name="validationQuery" value="SELECT 'x'" />
		<property name="testWhileIdle" value="true" />
		<property name="testOnBorrow" value="false" />
		<property name="testOnReturn" value="false" />

		<!-- 打開PSCache,而且指定每一個鏈接上PSCache的大小 -->
		<property name="poolPreparedStatements" value="true" />
		<property name="maxPoolPreparedStatementPerConnectionSize"
			value="20" />

		<!-- 配置監控統計攔截的filters,去掉後監控界面sql沒法統計 -->
		<property name="filters" value="stat" />
	</bean>
	
	<bean id="druidDynamicDataSource" class="com.eya.pubservice.datasource.DruidDynamicDataSource">
		<property name="defaultTargetDataSource" ref="defaultDataSource" />
        <property name="targetDataSources">
            <map>
                <entry key="defaultDataSource" value-ref="defaultDataSource"/>
                <!-- 這裏還能夠加多個dataSource -->
            </map>
        </property>
    </bean>    

	<!-- 註解事務 -->
	<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="druidDynamicDataSource" />
	</bean>

	<tx:annotation-driven transaction-manager="txManager" />

	<!-- 定義SqlSessionFactory -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="configLocation">
			<value>classpath:config/sqlMapConfig.xml</value>
		</property>
		<property name="dataSource" ref="druidDynamicDataSource" />
		<property name="typeAliasesPackage" value="com.eya.model.domain" />
		<property name="mapperLocations" value="classpath:com/eya/dao/**/*.xml" />
		<!-- define config location -->
		<!-- <property name="configLocation" value="sqlMapConfig.xml"/> -->
	</bean>
	<!-- 掃描mybatis的接口類 -->
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="com.eya.dao,com.eya.pubmapper" />
	</bean>
	<!-- spring 線程池的配置 -->
    <bean id ="taskExecutor"  class ="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor" >
        <!-- 線程池維護線程的最少數量 -->
        <property name ="corePoolSize" value ="5" />
        <!-- 線程池維護線程所容許的空閒時間 -->
        <property name ="keepAliveSeconds" value ="30000" />
        <!-- 線程池維護線程的最大數量 -->
        <property name ="maxPoolSize" value ="1000" />
        <!-- 線程池所使用的緩衝隊列 -->
        <property name ="queueCapacity" value ="200" />
    </bean>

    <!-- 配置線程池 -->
    <bean id ="dataImportTaskExecutor"  parent="taskExecutor" >
        <!-- 線程池維護線程的最少數量 -->
        <property name ="corePoolSize" value ="1" />
        <!-- 線程池維護線程的最大數量 -->
        <property name ="maxPoolSize" value ="1" />
    </bean>
</beans>

五、編寫測試類。實際業務中應該使用AOP實現數據源的切換,這裏只寫了一個測試,AOP相關很簡單,就不在這裏單獨寫了。當調用該方法時,能夠從日誌信息中看到,首先初始化了datasource-2,而且切換到了datasource-2。圖片效果不行,勉強看看mybatis

/**
 * 分頁查詢
 * @return {@link Pagination}
 * @Author : ll. create at 2016年04月05日 下午01:43:19
 */
@RequestMapping(value = "/page.do", method = RequestMethod.POST)
public Pagination<CoreRoleView> page(HttpServletRequest request) {
	logger.info("【分頁查詢】");

	Map<String, Object> map = new HashMap<String, Object>();
	map.put(DBContextHolder.DATASOURCE_KEY, "localhost");
	map.put(DBContextHolder.DATASOURCE_DRIVER, "com.mysql.jdbc.Driver");
	map.put(DBContextHolder.DATASOURCE_URL,
		"jdbc:mysql://127.0.0.1:3306/test_20170217?useUnicode=true&characterEncoding=UTF-8");
	map.put(DBContextHolder.DATASOURCE_USERNAME, "root");
	map.put(DBContextHolder.DATASOURCE_PASSWORD, "");
	DBContextHolder.setDBType(map);

	return super.page(request, false);
}
相關文章
相關標籤/搜索