【Java】一次SpringMVC+ Mybatis 配置多數據源經歷


需求

現在在維護的是學校的一款信息服務APP的后台,最近要開發一些新功能,其中一個就是加入學校電影院的在線購票。在線購票實際上已經有一套系統了,但是是外包給別人開發的,我們拿不到代碼只能拿到數據庫,並且也不一定能很好的兼容之前的代碼,所以需要基於這個數據庫來進行新的開發。

現在用的后台是SpringMVC+Mybatis+MySQL開發的,購票用的是SQL Server 2008(好古老的東西了),因為要用一套用戶體系所以不可能再去單獨為了這個功能弄一個系統出來,因此要在原項目中兼容這個數據庫。

問題

如何在一個web項目中使用兩個數據源,並且不同的接口可以按需選擇數據庫。

方案

最開始的做法

因為我們的項目用的是Mybatis作為ORM框架,在其配置文件中可以配置數據源信息,原始配置如下:

spring-mybatis.xml

<!-- 引入配置文件 -->
<bean id="propertyConfigurer"
	class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
	<property name="location" value="classpath:jdbc.properties" />
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
	<property name="driverClassName" value="${jdbc.driver}" />
	<property name="url" value="${jdbc.url}" />
	<property name="username" value="${jdbc.username}" />
	<property name="password" value="${jdbc.password}" />
	<!-- 初始化連接大小 -->
	<property name="initialSize" value="${jdbc.initialSize}"></property>
	<!-- 連接池最大數量 -->
	<property name="maxActive" value="${jdbc.maxActive}"></property>
	<!-- 連接池最大空閑 -->
	<property name="maxIdle" value="${jdbc.maxIdle}"></property>
	<!-- 連接池最小空閑 -->
	<property name="minIdle" value="${jdbc.minIdle}"></property>
	<!-- 獲取連接最大等待時間 -->
	<property name="maxWait" value="${jdbc.maxWait}"></property>
</bean>

<!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
	<property name="dataSource" ref="dataSource" />
	<!-- 自動掃描mapping.xml文件 -->
	<property name="mapperLocations" value="classpath:path/to/mapping/*.xml"></property>
</bean>

<!-- DAO接口所在包名,Spring會自動查找其下的類 -->
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<property name="basePackage" value="path.to.dao" />
	<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>

<!-- (事務管理)transaction manager, use JtaTransactionManager for global tx -->
<bean id="transactionManager"
	class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dataSource" />
</bean>

然后我就天真的認為是不是再新建一個dataSource的bean、sqlSessionFactory、mapperScannerConfigurer和transactionManager,把數據庫連接信息改一下,就可以同時使用兩個數據庫了。但是嘗試之后發現第二個數據庫的mapping文件根本沒有被初始化進spring的context中,報了Invalid bound statement (not found)這個錯,查了一下說是配置文件不對等原因造成的。后來發現實際上因為上面的配置文件中的sqlSessionFactory在spring中是單例的,因此按照我的想法第二個sqlSessionFactory根本就不會被實例化。所以此方法行不通!

改進做法

最后是在這篇博客中找到了正確可行的解決方法:使用Spring提供的AbstractRoutingDataSource類來根據請求路由到不同的數據源。具體做法是
先設置兩個不同的dataSource代表不同的數據源,再建一個總的dynamicDataSource,根據不同的請求去設置dynamicDataSource。代碼如下:

配置文件spring-mybatis.xml

<!--統一的dataSource-->
<bean id="dynamicDataSource" class="path.to.DynamicDataSource" >
    <property name="targetDataSources">
        <map key-type="java.lang.String">
			<!--通過不同的key決定用哪個dataSource-->
            <entry value-ref="dataSource" key="dataSource"></entry>
            <entry value-ref="mssqlDataSource" key="mssqlDataSource"></entry>
        </map>
    </property>
	<!--設置默認的dataSource-->
    <property name="defaultTargetDataSource" ref="dataSource">
    </property>
</bean>

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
	<property name="driverClassName" value="${jdbc.driver}" />
	<property name="url" value="${jdbc.url}" />
	<property name="username" value="${jdbc.username}" />
	<property name="password" value="${jdbc.password}" />
	<!-- 初始化連接大小 -->
	<property name="initialSize" value="${jdbc.initialSize}"></property>
	<!-- 連接池最大數量 -->
	<property name="maxActive" value="${jdbc.maxActive}"></property>
	<!-- 連接池最大空閑 -->
	<property name="maxIdle" value="${jdbc.maxIdle}"></property>
	<!-- 連接池最小空閑 -->
	<property name="minIdle" value="${jdbc.minIdle}"></property>
	<!-- 獲取連接最大等待時間 -->
	<property name="maxWait" value="${jdbc.maxWait}"></property>
</bean>

<!--電影票數據庫是mssql2008,單獨的數據庫,配置如下-->
<bean id="mssqlDataSource" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="${jdbc-mssql.driver}" />
    <property name="url" value="${jdbc-mssql.url}" />
    <property name="username" value="${jdbc-mssql.username}" />
    <property name="password" value="${jdbc-mssql.password}" />
    <!-- 初始化連接大小 -->
    <property name="initialSize" value="${jdbc.initialSize}"></property>
    <!-- 連接池最大數量 -->
    <property name="maxActive" value="${jdbc.maxActive}"></property>
    <!-- 連接池最大空閑 -->
    <property name="maxIdle" value="${jdbc.maxIdle}"></property>
    <!-- 連接池最小空閑 -->
    <property name="minIdle" value="${jdbc.minIdle}"></property>
    <!-- 獲取連接最大等待時間 -->
    <property name="maxWait" value="${jdbc.maxWait}"></property>
</bean>

<!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
	<property name="dataSource" ref="dynamicDataSource" />
	<!-- 自動掃描mapping.xml文件 -->
	<property name="mapperLocations" value="classpath:path/to/mapping/*.xml"></property>
</bean>

<!-- DAO接口所在包名,Spring會自動查找其下的類 -->
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<property name="basePackage" value="path.to.dao" />
	<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>

<!-- (事務管理)transaction manager, use JtaTransactionManager for global tx -->
<bean id="transactionManager"
	class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dynamicDataSource" />
</bean>

DynamicDataSource.java

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return CustomerContextHolder.getCustomerType();
    }
}

CustomerContextHolder.java

public class CustomerContextHolder {
    public static final String DATA_SOURCE_MYSQL = "dataSource";
    public static final String DATA_SOURCE_MSSQL = "mssqlDataSource";
	//用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_MYSQL;
        }else {
            return dataSource;
        }
    }
    public static void clearCustomerType() {
        contextHolder.remove();
    }
}

ServiceImpl.java

CustomerContextHolder.setCustomerType(CustomerContextHolder.DATA_SOURCE_MSSQL);

值得注意的是在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類也值得好好研究一下。

總結

其實這個方案不僅僅可以用來處理不同數據源的問題,同時業務量上來之后需要把數據庫進行主從分離或是把一個庫分為多個庫,都需要用到這樣的做法。這次暴露的問題確實也了解了不少,繼續學習吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM