近日,博主有個業務需求,就是根據數據庫存儲的不一樣數據源信息,動態建立數據源並實現業務不一樣而轉到不一樣的數據源上處理。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); }