在最近的項目業務中,需要在程序的運行過程中,添加新的數據庫添鏈接進來,然后從新數據庫鏈接中讀取數據。
網上查閱了資料,發現spring為多數據源提供了一個抽象類AbstractRoutingDataSource,該類中只有一個抽象方法determineCurrentLookupKey()需要由我們實現。
以下是使用方法
假設我們創建一個類DynimaticDataSource,繼承AbstractRoutingDataSource,並重寫determineCurrentLookupKey()方法。
spring啟動初始化DynimaticDataSource:
1、可以通過spring的自動注入,對AbstractRoutingDataSource類注入相應的屬性,注入屬性不是必須的,可以通過繼承的子類重寫這些方法來重新設置對這些屬性的調用。
2、注入后spring會執行protected方法afterPropertiesSet(),該方法先判斷屬性targetDataSources不能為null,即必須初始化時注入至少一個數據源,否則拋出異常"Property 'targetDataSources' is required"。然后將該屬性(類型為map<object,object>)的值全部轉換到屬性resolvedDataSources(類型為map<Object, DataSource>)中去。如果屬性defaultTargetDataSource不為null,即已經設置默認數據源,將其轉換為DataSource類型並賦值給屬性defaultTargetDataSource。
經過以上處理后,屬性resolvedDataSources中會被存放我們添加的數據源,該屬性是一個map集合,key為Object類型,value為數據源。同時可能會有一個默認數據源(可以注入也可以不注入)。
使用DynimaticDataSource類獲取連接:
1、調用該類的public方法getConnection()來獲取連接。
2、getConnection在抽象類中被重寫,會先調用protected方法determineTargetDataSource()。該方法先判斷屬性resolvedDataSources不為null,即初始化時候注入了至少一個數據源,否則拋出異常"DataSource router not initialized"。然后調用由子類重寫的抽象方法determineCurrentLookupKey()獲取dataSource在resolvedDataSources中對應的key,判斷使用哪個數據源。
3、根據key從resolvedDataSources中獲取數據源,如果resolvedDataSources中不存在,再判斷lenientFallback為true(默認為true,可以設置)或key為null,返回默認數據源resolvedDefaultDataSource。否則拋出異常"Cannot determine target DataSource for lookup key [" + key+ "]"。
4、調用獲取數據源的getConnection()方法獲取連接。
在初始化時指定多數據源案例代碼:
1、創建一個類DynimaticDataSource,繼承AbstractRoutingDataSource,並重寫determineCurrentLookupKey()方法。該方法負責判斷當前線程使用哪一種數據源。這是最簡單的一種實現方法,不重寫任何非抽象方法,但必須在初始化時配置至少一個的數據源。
public class DynamicDatasource extends AbstractRoutingDataSource{ private static Map<Object, Object> targetDataSources; protected DruidDataSource dataSource; @Override protected String determineCurrentLookupKey() {
//指定使用哪個數據源 return DataSourceUtils.getDbtype(); } }
2、srping配置文件(部分)
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <property name="driverClassName" value="${jdbc.driver}" /> <property name="maxActive" value="50" /><!-- 最大連接池數 --> <property name="minIdle" value="5" /><!-- 最小連接池數 --> </bean> <bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> <property name="url" value="${jdbc.url2}" /> <property name="username" value="${jdbc.username2}" /> <property name="password" value="${jdbc.password2}" /> <property name="driverClassName" value="${jdbc.driver}" /> <property name="maxActive" value="50" /><!-- 最大連接池數 --> <property name="minIdle" value="5" /><!-- 最小連接池數 --> </bean> <bean id="multDataSource" class="com.ftpSystem.dao.DynamicDatasource"> <property name="targetDataSources"> <map > <entry value-ref="dataSource" key="masterDataSource"></entry> <entry value-ref="dataSource2" key="yangDataSource"></entry> </map> </property>
<property name="defaultTargetDataSource" ref="dataSource"></property>
</bean>
3、使用數據源,我這里使用jdbcTemple,可以使用其他持久層框架,方式同單數據源配置一致。
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="multDataSource"></property> </bean>
4、切換數據源類
public class DataSourceUtils { private static final ThreadLocal<String> local = new ThreadLocal<String>(); public static String getDbtype() { return local.get(); } public static void setDbtype(String dbtype) { local.set(dbtype); } public static void clear() { local.remove(); } }
5、客戶端代碼
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext-dao.xml") public class TestDao { @Autowired private JdbcTemplate jdbcTemplate; @Test public void Test(){ System.out.println("ok"); int i = (Integer) jdbcTemplate.queryForObject("select count(*) from t_gg_zsdw", Integer.class); System.out.println("i="+i); DataSourceUtils.setDbtype("dataSource2");//切換數據源至dataSource2 i = (Integer) jdbcTemplate.queryForObject("select count(*) from t_gg_zsdw", Integer.class); System.out.println("i="+i); } }
由於我們已經在spring的配置文件中指定了屬性defaultTargetDataSource,因此程序會默認使用該數據源。然后我們執行一次后切換為dataSource2,之后的執行會改為使用dataSource2的數據源。
以上方式可以對程序配置多數據源,但是缺點在程序初始化之時就指定所有的數據源,無法在運行時動態添加。
程序動態配置多數據源:
通過最上面我們對獲取DynimaticDataSource對象的數據源連接的過程分析可知,AbstractRoutingDataSource類是通過determineTargetDataSource()方法來獲取數據源。在該方法里面,又使用了我們重寫的抽象方法determineCurrentLookupKey()來判斷使用哪一個數據源的key,通過這個key,在數據源初始化后存放的集合resolvedDataSources中獲取想要的數據源。
因此,如果我們想要動態的添加數據源進去,有兩種思路可以考慮,一是重寫determineTargetDataSource()方法,並且我們自己配置一個數據源集合,通過該方法,調用我們自己的數據源集合中對應的數據源。二是在resolvedDataSources屬性中動態添加key-value進去,可以在determineTargetDataSource()方法中獲取該數據源即可。
通過對源碼查看可知,第二種思路所需的屬性resolvedDataSources是私有的,且類中沒有提供相應的get方法獲取。因此只能采用第一種方式。
樣例如下:
先是繼承AbstractRoutingDataSource的實體類
/** * 數據源集合管理類 * 使用數據源之前必須先添加,然后指定使用哪個數據源 * @author yangxiangfeng * */ public class DynamicDatasource extends AbstractRoutingDataSource { private static Map<String, DataSource> dataSourceMap = new HashMap<String, DataSource>(); private final static Map<DataSourceEntity, String> dseMap = new HashMap<>();
//spring注入需要set方法,不是必要的,可以注入也可以不注入 public static void setDataSourceMap(Map<String, DataSource> dataSourceMap) { DynamicDatasource.dataSourceMap = dataSourceMap; }
//獲取數據源信息集合 public static Map<DataSourceEntity, String> getDsemap() { return dseMap; }
//檢查是否包含指定id的數據源 public static boolean checkDbKey(String dbKey){ if(dataSourceMap.get(dbKey) != null)return true; return false; } //抽象方法,必須重寫,用來判斷使用哪個數據源 @Override protected String determineCurrentLookupKey() { return DataSourceUtils.getDbKey(); } /** * 對數據源的初始化方法,由於這里已經將數據源集合放在本類中,如果不重寫將會由於父類參數為null而拋出異常。 */ @Override public void afterPropertiesSet() {} /** * 確定使用哪一個數據源 * 這里不做null判斷,因為是經過null判斷后再進入的。 */ @Override protected DataSource determineTargetDataSource() { System.out.println("tttt"); String dsKey = determineCurrentLookupKey(); DataSource dds = dataSourceMap.get(dsKey); return dds; } /** * 添加數據源 * 為了防止多線程添加同一個數據源,這里采用同步,同時會判斷是否已存在 * @param dbkey * @param ip * @param port * @param service 實例名 * @param username * @param password * @return String 新建數據源對應的key,如果已經存在,則返回之前的key。 */ public synchronized String addDataSource(String dbkey, String ip, int port, String service, String username, String password){ DataSourceEntity d1 = new DataSourceEntity(ip, port, service, username); String value = dseMap.get(d1); if(dseMap.get(d1) != null){ return value;//已存在則返回該數據源的id } DataSource ds = createDataSource(ip, port, service, username, password); dataSourceMap.put(dbkey, ds);//存儲數據源集合 dseMap.put(d1, dbkey);//保存已經存儲了哪些數據源 return dbkey; } /** * 創建一個數據源 * @param ip * @param port * @param service * @param username * @param password * @return */ private DataSource createDataSource(String ip, int port, String service, String username, String password){ DruidDataSource dds = new DruidDataSource(); dds.setDriverClassName("oracle.jdbc.driver.OracleDriver"); dds.setUrl("jdbc:oracle:thin:@"+ip+":"+port+":"+service); dds.setUsername(username); dds.setPassword(password); return dds; }
2、切換數據源的工具類
public class DataSourceUtils { private static final ThreadLocal<String> local = new ThreadLocal<String>(); public static String getDbKey() { return local.get(); } public static void setDbKey(String dbKey) { if(DynamicDatasource.checkDbKey(dbKey)){ local.set(dbKey); } else { throw new NullPointerException("不存在id為\""+dbKey+"\"的數據源!");
} } public static void clear() { local.remove(); } }
客戶端測試類
@Test public void Test(){ System.out.println("ok"); //增加數據源 dynamicDatasource.addDataSource("dataSource", "***.***.**.***", ****, "***", "***", "*****"); //指定使用的數據源 DataSourceUtils.setDbKey("dataSource"); //執行sql int i = (Integer) jdbcTemplate.queryForObject("select count(*) from tablename", Integer.class); System.out.println("i="+i); }
以上代碼是經過測試后可以直接運行的,由於本人能力有限,綜合考慮后對spring中動態創建數據源的實現如上。歡迎各位指點可以優化改進的地方。