背景
最近在项目中遇到在同一个工程中需要用到两个数据库的情况,在查询了众多资料后做个总结。参考http://blog.51cto.com/linhongyu/1615895
首先,回顾一下单数据库的配置情况
先配置数据源
<bean id="adminDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="${mifi_admin_master_db_url}"/>
<property name="username" value="${mifi_admin_master_db_username}"/>
<property name="password" value="${mifi_admin_master_db_password}"/>
<property name="validationQuery" value="SELECT 1"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<property name="testWhileIdle" value="true"/>
<property name="timeBetweenEvictionRunsMillis" value="300000"/>
<property name="minEvictableIdleTimeMillis" value="1800000"/>
<property name="numTestsPerEvictionRun" value="-1"/>
<property name="initialSize" value="20"/>
<property name="maxActive" value="150"/>
<property name="maxIdle" value="20"/>
<property name="minIdle" value="10"/>
<property name="maxWait" value="1000"/>
</bean>
然后配置sqlSessionFactory
<bean id="adminSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="adminDataSource"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
再配置mapperScannerConfigurer
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.xiaomi.mifi.ins.neo.lucky.dao"/>
<property name="sqlSessionFactoryBeanName" value="adminSqlSessionFactory"/>
</bean>
现在由于SqlSessionFactoryBean是单例的,所以不能配置两个SqlSessionFactoryBean。所以思路是配置两个数据源,然后配置一个DynamicDataSource动态决定用哪个数据源
1.配置两个数据源
<!-- mifi_insurance -->
<bean id="insuranceDataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="${mifi_insurance_master_url}"/>
<property name="username" value="${mifi_insurance_master_username}"/>
<property name="password" value="${mifi_insurance_master_password}"/>
<property name="validationQuery" value="SELECT 1"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<property name="testWhileIdle" value="true"/>
<property name="timeBetweenEvictionRunsMillis" value="300000"/>
<property name="minEvictableIdleTimeMillis" value="1800000"/>
<property name="numTestsPerEvictionRun" value="-1"/>
<property name="initialSize" value="20"/>
<property name="maxActive" value="150"/>
<property name="maxIdle" value="20"/>
<property name="minIdle" value="10"/>
<property name="maxWait" value="1000"/>
</bean>
<!-- mifi_insurance_activity -->
<bean id="activityDataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="${mifi_insurance_activity_master_url}"/>
<property name="username" value="${mifi_insurance_activity_master_username}"/>
<property name="password" value="${mifi_insurance_activity_master_password}"/>
<property name="validationQuery" value="SELECT 1"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<property name="testWhileIdle" value="true"/>
<property name="timeBetweenEvictionRunsMillis" value="300000"/>
<property name="minEvictableIdleTimeMillis" value="1800000"/>
<property name="numTestsPerEvictionRun" value="-1"/>
<property name="initialSize" value="20"/>
<property name="maxActive" value="150"/>
<property name="maxIdle" value="20"/>
<property name="minIdle" value="10"/>
<property name="maxWait" value="1000"/>
</bean>
2.配置DynamicDataSource
先来看看DynamicDataSource的原理,DynamicDataSource继承自AbstractRoutingDataSource。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean
而AbstractRoutingDataSource继承自AbstractDataSource,我们可以重点看他的getConnection方法:
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
获取连接的方法中,重点是determineTargetDataSource()方法,看源码:
/**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
上面这段源码的重点在于determineCurrentLookupKey()方法,这是AbstractRoutingDataSource类中的一个抽象方法,而它的返回值是你所要用的数据源dataSource的key值,有了这个key值,resolvedDataSource(这是个map,由配置文件中设置好后存入的)就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。
看完源码,应该有点启发了吧,没错!你要扩展AbstractRoutingDataSource类,并重写其中的determineCurrentLookupKey()方法,来实现数据源的切换:
<bean id="dynamicDataSource" class="com.xiaomi.mifi.ins.neo.ins3c.util.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<!--通过不同的key决定用哪个dataSource-->
<entry value-ref="insuranceDataSource" key="insuranceDataSource"></entry>
<entry value-ref="activityDataSource" key="activityDataSource"></entry>
</map>
</property>
<!--设置默认的dataSource-->
<property name="defaultTargetDataSource" ref="insuranceDataSource">
</property>
</bean>
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContext.getDataSource();
}
}
我们来看一下DataSourceContext的代码
public class DataSourceContext {
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public static void setDataSource(String dataSource) {
THREAD_LOCAL.set(dataSource);
}
public static String getDataSource() {
return THREAD_LOCAL.get();
}
public static void clearDataSource() {
THREAD_LOCAL.remove();
}
}
主要关注其中的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);
}
现在我们只要在恰当是时间把数据源的key放入其中就可以了,那么在什么时候放入呢,当然是service层的方法中放入比较好。如果每次在代码中手动放入的会比较麻烦,可以写个自定义注解
package com.xiaomi.mifi.ins.neo.ins3c.util.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by zhangjian on 2019/1/4.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String name() default "insuranceDataSource";
public static String insurance = "insuranceDataSource";
public static String activity = "activityDataSource";
}
这样,每次在service层的方法上加@DataSource(name=“......”)就可以了。
关于自定义spring自定义注解,可以参见这篇文章