1. 程式人生 > >【Java】一次SpringMVC+ ibatis 配置多資料來源經歷

【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類也值得好好研究一下。