【Java】一次SpringMVC+ ibatis 配置多資料來源經歷
問題
如何在一個web專案中使用兩個資料來源,並且不同的介面可以按需選擇資料庫。
方案
最開始的做法
因為我們的專案用的是ibatis作為ORM框架,在其配置檔案中可以配置資料來源資訊,原始配置如下:
spring-application.xml
<!-- DBCP資料來源 --> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbcMysql.driverClassName}" /> <property name="url" value="${jdbcMysql.url}" /> <property name="username" value="${jdbcMysql.username}" /> <property name="password" value="${jdbcMysql.password}" /> <property name="initialSize" value="${jdbcMysql.initialSize}" /> <property name="maxActive" value="${jdbcMysql.maxActive}" /> <property name="maxIdle" value="${jdbcMysql.maxIdle}" /> <property name="maxWait" value="${jdbcMysql.maxWait}" /> <!-- 連線空閒時候對空閒連線定期檢查,無效的連線被剔除,保證連線的有效性,單位是毫秒 (建議5-30秒定期檢查) update by zhanghu --> <property name="timeBetweenEvictionRunsMillis"> <value>30000</value> </property> <!-- 當空閒連線未使用的時間超過此值以後,空閒連線被關閉,並重新建立空閒連線,此引數防止伺服器段超時關閉客戶端的SQL連線,單位是毫秒 (建議10-30分鐘) add by zhanghu --> <property name="minEvictableIdleTimeMillis"> <value>1800000</value> </property> <!-- 在每次空閒連接回收器執行緒(如果有)執行時檢查的連線數量,最好和maxActive一致 add by zhanghu--> <property name="numTestsPerEvictionRun" value="${jdbcMysql.maxActive}" /> <property name="testWhileIdle"> <value>true</value> </property> <property name="validationQuery"> <value>select 1 from dual</value> </property> <property name="testOnBorrow"> <value>false</value> </property> <!--removeAbandoned: 是否自動回收超時連線--> <property name="removeAbandoned" value="true"/> <!--removeAbandonedTimeout: 超時時間(以秒數為單位)--> <property name="removeAbandonedTimeout" value="300"/> </bean> <!-- spring和ibatis整合 注入資料來源到SqlMapClient工廠 --> <bean id="sqlMapClientFactory" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> <!-- <property name="dataSource" ref="dataSource" /> --> <property name="dataSource" ref="dynamicDataSource" /> <property name="configLocation" value="classpath:/META-INF/ibatis/ibatis-sqlMapConfig.xml" /> </bean> <!-- 將SqlMapClient工廠建立的物件例項注入到介面層 --> <bean id="sqlMapClientDao" class="com.ake.dccs.ecs.dao.impl.SqlMapClientDaoImpl"> <property name="sqlMapClient" ref="sqlMapClientFactory" /> </bean> <!-- 將訪問層物件例項注入到業務層 --> <bean id="sqlMapClientService" class="com.ake.dccs.ecs.service.common.SqlMapClientServiceImpl"> <property name="sqlMapClientDao" ref="sqlMapClientDao" /> </bean> <bean id="serviceInstance" class="com.ake.dccs.ecs.service.common.SqlMapClientInstanceUtil"> <property name="serviceInstance" ref="sqlMapClientService" /> </bean> <tx:annotation-driven transaction-manager="txManager" /> <!-- 配置jdbc資料來源的事務管理器 --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <aop:config> <aop:pointcut id="serviceMethods" expression="execution(* com.ake.dccs.ecs.service..*.*(..)) " /> <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods" /> </aop:config> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="get*" read-only="true" /> <tx:method name="query*" read-only="true"/> <tx:method name="add*" rollback-for="Exception" propagation="REQUIRED" /> <tx:method name="save*" rollback-for="Exception" propagation="REQUIRED" /> <tx:method name="update*" rollback-for="Exception" propagation="REQUIRED" /> <tx:method name="del*" rollback-for="Exception" propagation="REQUIRED" /> <tx:method name="insert*" rollback-for="Exception" propagation="REQUIRED" /> </tx:attributes> </tx:advice>
然後我就天真的認為是不是再新建一個dataSource的bean、sqlSessionFactory、mapperScannerConfigurer和transactionManager,把資料庫連線資訊改一下,就可以同時使用兩個資料庫了。但是嘗試之後發現第二個資料庫的mapping檔案根本沒有被初始化進spring的context中,報了Invalid bound statement (not found)這個錯,查了一下說是配置檔案不對等原因造成的。後來發現實際上因為上面的配置檔案中的sqlSessionFactory在spring中是單例的,因此按照我的想法第二個sqlSessionFactory根本就不會被例項化。所以此方法行不通!
改進做法
最後是在這篇部落格中找到了正確可行的解決方法:使用Spring提供的AbstractRoutingDataSource類來根據請求路由到不同的資料來源。具體做法是
先設定兩個不同的dataSource代表不同的資料來源,再建一個總的dynamicDataSource,根據不同的請求去設定dynamicDataSource。程式碼如下:
spring-application.xml
<!-- DBCP資料來源 --> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbcMysql.driverClassName}" /> <property name="url" value="${jdbcMysql.url}" /> <property name="username" value="${jdbcMysql.username}" /> <property name="password" value="${jdbcMysql.password}" /> <property name="initialSize" value="${jdbcMysql.initialSize}" /> <property name="maxActive" value="${jdbcMysql.maxActive}" /> <property name="maxIdle" value="${jdbcMysql.maxIdle}" /> <property name="maxWait" value="${jdbcMysql.maxWait}" /> <!-- 連線空閒時候對空閒連線定期檢查,無效的連線被剔除,保證連線的有效性,單位是毫秒 (建議5-30秒定期檢查) update by zhanghu --> <property name="timeBetweenEvictionRunsMillis"> <value>30000</value> </property> <!-- 當空閒連線未使用的時間超過此值以後,空閒連線被關閉,並重新建立空閒連線,此引數防止伺服器段超時關閉客戶端的SQL連線,單位是毫秒 (建議10-30分鐘) add by zhanghu --> <property name="minEvictableIdleTimeMillis"> <value>1800000</value> </property> <!-- 在每次空閒連接回收器執行緒(如果有)執行時檢查的連線數量,最好和maxActive一致 add by zhanghu--> <property name="numTestsPerEvictionRun" value="${jdbcMysql.maxActive}" /> <property name="testWhileIdle"> <value>true</value> </property> <property name="validationQuery"> <value>select 1 from dual</value> </property> <property name="testOnBorrow"> <value>false</value> </property> <!--removeAbandoned: 是否自動回收超時連線--> <property name="removeAbandoned" value="true"/> <!--removeAbandonedTimeout: 超時時間(以秒數為單位)--> <property name="removeAbandonedTimeout" value="300"/> </bean> <bean id="dataSourceTwo" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${dbTwo.jdbcMysql.driverClassName}" /> <property name="url" value="${dbTwo.jdbcMysql.url}" /> <property name="username" value="${dbTwo.jdbcMysql.username}" /> <property name="password" value="${dbTwo.jdbcMysql.password}" /> <property name="initialSize" value="${jdbcMysql.initialSize}" /> <property name="maxActive" value="${jdbcMysql.maxActive}" /> <property name="maxIdle" value="${jdbcMysql.maxIdle}" /> <property name="maxWait" value="${jdbcMysql.maxWait}" /> <!-- 連線空閒時候對空閒連線定期檢查,無效的連線被剔除,保證連線的有效性,單位是毫秒 (建議5-30秒定期檢查) update by zhanghu --> <property name="timeBetweenEvictionRunsMillis"> <value>30000</value> </property> <!-- 當空閒連線未使用的時間超過此值以後,空閒連線被關閉,並重新建立空閒連線,此引數防止伺服器段超時關閉客戶端的SQL連線,單位是毫秒 (建議10-30分鐘) add by zhanghu --> <property name="minEvictableIdleTimeMillis"> <value>1800000</value> </property> <!-- 在每次空閒連接回收器執行緒(如果有)執行時檢查的連線數量,最好和maxActive一致 add by zhanghu--> <property name="numTestsPerEvictionRun" value="${jdbcMysql.maxActive}" /> <property name="testWhileIdle"> <value>true</value> </property> <property name="validationQuery"> <value>select 1 from dual</value> </property> <property name="testOnBorrow"> <value>false</value> </property> <!--removeAbandoned: 是否自動回收超時連線--> <property name="removeAbandoned" value="true"/> <!--removeAbandonedTimeout: 超時時間(以秒數為單位)--> <property name="removeAbandonedTimeout" value="300"/> </bean> <bean id="dynamicDataSource" class="com.ake.dccs.ecs.service.common.DynamicDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <!-- 通過不同的key決定用哪個dataSource --> <entry value-ref="dataSource" key="dataSource"></entry> <entry value-ref="dataSourceTwo" key="dataSourceTwo"></entry> </map> </property> <!-- 設定預設的dataSource --> <property name="defaultTargetDataSource" ref="dataSource"></property> </bean> <!-- spring和ibatis整合 注入資料來源到SqlMapClient工廠 --> <bean id="sqlMapClientFactory" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> <!-- <property name="dataSource" ref="dataSource" /> --> <property name="dataSource" ref="dynamicDataSource" /> <property name="configLocation" value="classpath:/META-INF/ibatis/ibatis-sqlMapConfig.xml" /> </bean> <!-- 將SqlMapClient工廠建立的物件例項注入到介面層 --> <bean id="sqlMapClientDao" class="com.ake.dccs.ecs.dao.impl.SqlMapClientDaoImpl"> <property name="sqlMapClient" ref="sqlMapClientFactory" /> </bean> <!-- 將訪問層物件例項注入到業務層 --> <bean id="sqlMapClientService" class="com.ake.dccs.ecs.service.common.SqlMapClientServiceImpl"> <property name="sqlMapClientDao" ref="sqlMapClientDao" /> </bean> <bean id="serviceInstance" class="com.ake.dccs.ecs.service.common.SqlMapClientInstanceUtil"> <property name="serviceInstance" ref="sqlMapClientService" /> </bean> <tx:annotation-driven transaction-manager="txManager" /> <!-- 配置jdbc資料來源的事務管理器 --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- <property name="dataSource" ref="dataSource" /> --> <property name="dataSource" ref="dynamicDataSource" /> </bean> <aop:config> <aop:pointcut id="serviceMethods" expression="execution(* com.ake.dccs.ecs.service..*.*(..)) " /> <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods" /> </aop:config> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="get*" read-only="true" /> <tx:method name="query*" read-only="true"/> <tx:method name="add*" rollback-for="Exception" propagation="REQUIRED" /> <tx:method name="save*" rollback-for="Exception" propagation="REQUIRED" /> <tx:method name="update*" rollback-for="Exception" propagation="REQUIRED" /> <tx:method name="del*" rollback-for="Exception" propagation="REQUIRED" /> <tx:method name="insert*" rollback-for="Exception" propagation="REQUIRED" /> </tx:attributes> </tx:advice>
DynamicDataSource.java
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return CustomerContextHolder.getCustomerType();
}
}
CustomerContextHolder.java
import org.apache.commons.lang.StringUtils;
public class CustomerContextHolder {
public static final String DATA_SOURCE_DCCSGDS = "dataSource";
public static final String DATA_SOURCE_IEMS = "dataSourceTwo";
//用ThreadLocal來設定當前執行緒使用哪個dataSource
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
/**設定要使用的資料來源*/
public static void setCustomerType(String customerType) {
contextHolder.set(customerType);
}
/**獲取資料來源*/
public static String getCustomerType() {
String dataSource = contextHolder.get();
if (StringUtils.isEmpty(dataSource)) {
return DATA_SOURCE_DCCSGDS;
}else {
return dataSource;
}
}
/**清除資料來源,使用預設的資料來源*/
public static void clearCustomerType() {
contextHolder.remove();
}
}
ServiceImpl.java
CustomerContextHolder.setCustomerType(CustomerContextHolder.DATA_SOURCE_IEMS);//切換資料來源
CustomerContextHolder.clearCustomerType();//清除資料來源,恢復預設資料來源
值得注意的是在CustomerContextHolder.java中使用了ThreadLocal類的set方法來設定當前執行緒要選擇的dataSource,看一下set方法的原始碼:
ThreadLocal.set()
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
顯而易見,獲取當前執行緒,並且使用一個hashmap把需要儲存的值設定進去。因為tomcat是用的執行緒池來處理每個請求,所以用ThreadLocal可以保證執行緒安全問題。同時這個AbstractRoutingDataSource類也值得好好研究一下。