spring多資料來源配置
專案中我們經常會遇到多資料來源的問題,尤其是資料同步或定時任務等專案更是如此。多資料來源讓人最頭痛的,不是配置多個數據源,而是如何能靈活動態的切換資料來源。例如在一個spring和hibernate的框架的專案中,我們在spring配置中往往是配置一個dataSource來連線資料庫,然後繫結給sessionFactory,在dao層程式碼中再指定sessionFactory來進行資料庫操作。
二、程式碼實現
1.
首先在配置檔案中配置多個dataSource
<bean id="dataSourceMySql" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass" value="${mysql.jdbc.driverClass}" /> <property name="jdbcUrl" value="${mysql.jdbc.url}" /> <property name="user" value="${mysql.jdbc.user}" /> <property name="password" value="${mysql.jdbc.password}" /> <property name="initialPoolSize" value="${mysql.jdbc.initialPoolSize}" /> <property name="minPoolSize" value="${mysql.jdbc.minPoolSize}" /> <property name="maxPoolSize" value="${mysql.jdbc.maxPoolSize}" /> <property name="checkoutTimeout" value="${mysql.jdbc.checkoutTimeout}" /> <property name="idleConnectionTestPeriod" value="${mysql.jdbc.idleConnectionTestPeriod}" /> <property name="maxIdleTime" value="${mysql.jdbc.maxIdleTime}" /> <property name="maxStatements" value="${mysql.jdbc.maxStatements}" /> <property name="testConnectionOnCheckout" value="${mysql.jdbc.testConnectionOnCheckout}" /> </bean> <bean id="dataSourceOracle" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass" value="${oracle.jdbc.driverClass}" /> <property name="jdbcUrl" value="${oracle.jdbc.url}" /> <property name="user" value="${oracle.jdbc.user}" /> <property name="password" value="${oracle.jdbc.password}" /> <property name="initialPoolSize" value="${oracle.jdbc.initialPoolSize}" /> <property name="minPoolSize" value="${oracle.jdbc.minPoolSize}" /> <property name="maxPoolSize" value="${oracle.jdbc.maxPoolSize}" /> <property name="checkoutTimeout" value="${oracle.jdbc.checkoutTimeout}" /> <property name="idleConnectionTestPeriod" value="${oracle.jdbc.idleConnectionTestPeriod}" /> <property name="maxIdleTime" value="${oracle.jdbc.maxIdleTime}" /> <property name="maxStatements" value="${oracle.jdbc.maxStatements}" /> <property name="testConnectionOnCheckout" value="${oracle.jdbc.testConnectionOnCheckout}" /> </bean>
2. 擴充套件Spring的AbstractRoutingDataSource抽象類,實現動態資料來源。
AbstractRoutingDataSource中的抽象方法determineCurrentLookupKey是實現資料來源的route的核心.這裡對該方法進行Override。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DatabaseContextHolder.getCustomerType(); } }
上下文DatabaseContextHolder為一執行緒安全的ThreadLocal,具體程式碼如下:
public class DatabaseContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); public static void setCustomerType(String customerType) { contextHolder.set(customerType); } public static String getCustomerType() { return contextHolder.get(); } public static void clearCustomerType() { contextHolder.remove(); } }
3. 配置動態資料來源
將DynamicDataSource Bean加入到Spring的上下文xml配置檔案中去,同時配置DynamicDataSource的targetDataSources(多資料來源目標)屬性的Map對映。
<bean id="dataSource" class="com.core.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="dataSourceMySql" value-ref="dataSourceMySql" />
<entry key="dataSourceOracle" value-ref="dataSourceOracle" />
</map>
</property>
<property name="defaultTargetDataSource" ref="dataSourceMySql" />
</bean>
4. 使用動態資料來源
DynamicDataSource是繼承與AbstractRoutingDataSource,而AbstractRoutingDataSource又是繼承於org.springframework.jdbc.datasource.AbstractDataSource,AbstractDataSource實現了統一的DataSource介面,所以DynamicDataSource同樣可以當一個DataSource使用。
<!-- JdbcTemplate使用動態資料來源的配置 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource">
<ref bean="dataSource" />
</property>
</bean>
<!-- 對JdbcTemplate的應用封裝類 -->
<bean id="sqlBaseDAO" class="com.whty.dao.BaseDAOImpl">
<property name="jdbcTemplate">
<ref bean="jdbcTemplate" />
</property>
</bean>
5.
動態資料來源的管理如何選擇控制每個業務中需要的具體資料來源,可是使用手動控制:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
BaseDAO dao = (BaseDAO) context.getBean("baseDAO", BaseDAOImpl.class);
try {
DatabaseContextHolder.setCustomerType("dataSourceMySql");
System.err.println(dao.select("select count(*) sum from TEST t ").get(0).get("SUM"));
DatabaseContextHolder.setCustomerType("dataSourceOracle");
System.err.println(dao.select("select count(*) sum from TEST t ").get(0).get("SUM"));
} catch (Exception e) {
e.printStackTrace();
}
如果在service層有比較統一的規則的話,也可以使用aop設定資料來源使用。
<bean id="dataSourceInterceptor" class="com.core.DataSourceInterceptor" />
<aop:config>
<aop:aspect id="dataSourceAspect" ref="dataSourceInterceptor">
<aop:pointcut id="dsMysql" expression="execution(* com.controller.mysql.*.*(..))" />
<aop:pointcut id="dsOracle" expression="execution(* com.controller.oracle.*.*(..))" />
<aop:before method="setdataSourceMysql" pointcut-ref="dsMysql"/>
<aop:before method="setdataSourceOracle" pointcut-ref="dsOracle"/>
</aop:aspect>
</aop:config>
import org.aspectj.lang.JoinPoint;
public class DataSourceInterceptor {
public void setdataSourceMysql(JoinPoint jp) {
DatabaseContextHolder.setCustomerType("dataSourceMySql");
}
public void setdataSourceOracle(JoinPoint jp) {
DatabaseContextHolder.setCustomerType("dataSourceOracle");
}
}
這裡一般都是一個service一個數據源,所以最好使用aop在service層執行完之後統一呼叫
DBContextHolder.clearDBType();
清空資料來源資訊。
三、注意事項
1. 注意事務攔截器的配置
這是首要的一條。首先你要明白,Spring的事務管理是與資料來源繫結的,一旦程式執行到事務管理的那一層(如service)的話,由於在進入該層之前事務已經通過攔截器開啟,因此在該層切換資料來源是不行的,明白事務的原理是尤為重要的,我之前的文章中,將切換資料來源的攔截器配置在了Dao層是有問題的(因為是示例,所以粗心了,對誤導了大家我表示道歉),但提供的思路是沒有問題的。
Demo中將切換資料來源的攔截器(dataSourceInterceptor)配置在了事務攔截器(txadvice)的上一層,也就是Controller層。
2. 注意資料庫表的建立
一些人喜歡用Hibernate的自動建立表的功能,但需要注意,在多資料來源中,尤其是不同資料庫的多資料來源,想都自動建表是不行的。因為Hibernate自動建表是在專案啟動時觸發的,因此只會建立專案配置的預設資料來源的表,而其他資料來源的表則不會自動建立。大家要注意著點。
3. Hibernate的資料庫方言(dialect)可以忽略
在多資料來源時,方言的設定可以忽略,Hibernate在使用時會自動識別不同的資料庫,因此不必糾結這個配置,甚至不配置也可以。
4. 報No current session錯誤
這個是因為使用了sessionFactory.getCurrentSession()導致的,current session是與執行緒繫結的,一個執行緒只會開啟一個Session(除非使用openSession()就不會報錯),因此需要設定session與執行緒的繫結關係。
Demo中使用了Spring管理Hibernate的session,因此在web.xml中配置了OpenSessionInViewFilter,並在hibernate.cfg.xml中配置了current_session_context_class。【PS:使用Spring管理Hibernate時,可以去掉hibernate.cfg.xml,而全部配置的Spring的配置檔案裡,即hibernateProperties。看個人喜好吧】