Spring学习(一): 使用AbstractRoutingDataSource类进行多数据源配置


多数据源在项目中很常见,举几个需求,如页面展示的数据需要从不同的数据库中去查询,修改等,例如数据库的读写分离等


配置多数据源要考虑到如下的情况
单个数据源是如下这种情况:

如果是多个数据源的话,将会变成下面这种情况



这样,每增加一个数据源就要去配置一个sessionFactory,这样比较麻烦,于是spring的
AbstractRoutingDataSource类产生了


它的实现原理是扩展Spring的`AbstractRoutingDataSource`抽象类(该类充当了`DataSource`的路由中介, 能在运行时, 根据某种key值来动态切换到真正的`DataSource`上。)
 从AbstractRoutingDataSource的源码中:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean


我们可以看到,它继承了AbstractDataSource,而AbstractDataSource不就是javax.sql.DataSource的子类,So我们可以分析下它的getConnection方法:
public Connection getConnection() throws SQLException {  
return determineTargetDataSource().getConnection();
}

public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}


获取连接的方法中,重点是determineTargetDataSource()方法,看源码:
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()方法,来实现数据源的切换:

接下来,将展示一个多数据源配置demo 

1.配置一个config类,配置3个数据源(下面是JAVA配置写法)
/**
* Created by Youjie on 2017/7/23.
*/
@Configuration
@EnableTransactionManagement
public class DataSourceConfig {

@Bean
@ConfigurationProperties(prefix = "datasource.db_a")
@Qualifier("dataSourceDefault")
public DataSource dataSourceDefault(){
return new DruidDataSource();
}

@Bean
@ConfigurationProperties(prefix = "datasource.db_b")
@Qualifier("dataSourceB")
public DataSource dataSourceB(){
return new DruidDataSource();
}

@Bean
@ConfigurationProperties(prefix = "datasource.db_c")
@Qualifier("dataSourceC")
public DataSource dataSourceC(){
return new DruidDataSource();
}


@Bean
@Primary
public DataSource multipleDataSource(){
DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
dynamicDataSource.setDefaultTargetDataSource(dataSourceDefault());

Map<Object,Object> dataSourcesType = new HashMap();
dataSourcesType.put(DataSources.DATASOURCE_A,dataSourceDefault());
dataSourcesType.put(DataSources.DATASOURCE_B,dataSourceB());
dataSourcesType.put(DataSources.DATASOURCE_C,dataSourceC());

dynamicDataSource.setTargetDataSources(dataSourcesType);

return dynamicDataSource;
}



@Bean
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(multipleDataSource());
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));
sqlSessionFactoryBean.setTypeAliasesPackage("com.may.model");
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("configuration.xml"));
return sqlSessionFactoryBean.getObject();
}

@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(multipleDataSource());
}



}
对应的 application.properties如下
server.port=9002
server.context-path=/


#A user
datasource.db_a.driver-class-name=com.mysql.jdbc.Driver
datasource.db_a.url=jdbc:mysql://127.0.0.1:3306/test_A?autoReconnect=true&useUnicode=true&characterEncoding=utf8&logSlowQueries=false&slowQueryThresholdMillis=0&allowMultiQueries=true
datasource.db_a.username=root
datasource.db_a.password=123456

#B city
datasource.db_b.driver-class-name=com.mysql.jdbc.Driver
datasource.db_b.url=jdbc:mysql://127.0.0.1:3306/test_B?autoReconnect=true&useUnicode=true&characterEncoding=utf8&logSlowQueries=false&slowQueryThresholdMillis=0&allowMultiQueries=true
datasource.db_b.username=root
datasource.db_b.password=123456

#C role
datasource.db_c.driver-class-name=com.mysql.jdbc.Driver
datasource.db_c.url=jdbc:mysql://127.0.0.1:3306/test_C?autoReconnect=true&useUnicode=true&characterEncoding=utf8&logSlowQueries=false&slowQueryThresholdMillis=0&allowMultiQueries=true
datasource.db_c.username=root
datasource.db_c.password=123456


2、写一个ThreadLocalDynamicDataSource类继承AbstractRoutingDataSource,并实现determineCurrentLookupKey方法
public class ThreadLocalRountingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceTypeManager.get();
}
}
3、编写DataSourceTypeManager执行类,并利用ThreadLocal以空间换时间的方式解决线程安全问题
public class DataSourceTypeManager {
private static final ThreadLocal<DataSources> dataSourceTypes = new ThreadLocal<DataSources>() {
@Override
protected DataSources initialValue() {
return DataSources.DATASOURCE_DEFAULT;
}
};

public static DataSources get() {
return dataSourceTypes.get();
}

public static void set(DataSources dataSourceType) {
dataSourceTypes.set(dataSourceType);
}

public static void reset() {
dataSourceTypes.set(DataSources.DATASOURCE_DEFAULT);
}

public static void clearDataSources () {
dataSourceTypes.remove();
}
}




上面代码利用initialValue方法,调用初始化时指定的DataSources 类型,这里DataSources 为枚举类型,里面定义三种数据源的名称,代码如下:
public enum DataSources {
DATASOURCE,DATASOURCE_DEFAULT,DATASOURCE_ORACLE
}

4、multipleDataSource多数据源配置的xml配置
<bean id="multipleDataSource"
class="com.hxqc.basic.dependency.datasource.ThreadLocalRountingDataSource">
<property name="defaultTargetDataSource" ref="ncDataSource"/>
<property name="targetDataSources">
<map key-type="com.hxqc.basic.dependency.datasource.DataSources">
<entry key="DATASOURCE" value-ref="erpDataSource"/>
<entry key="DATASOURCE_DEFAULT" value-ref="ncDataSource"/>
<entry key="DATASOURCE_ORACLE" value-ref="oracleDataSource"/>
</map>
</property>
</bean>



multipleDataSource多数据源配置的Java配置(上文已经贴出该配置,这里再强调一下)
@Bean
@Primary
public DataSource multipleDataSource(){
DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
dynamicDataSource.setDefaultTargetDataSource(dataSourceDefault());

Map<Object,Object> dataSourcesType = new HashMap();
dataSourcesType.put(DataSources.DATASOURCE_A,dataSourceDefault());
dataSourcesType.put(DataSources.DATASOURCE_B,dataSourceB());
dataSourcesType.put(DataSources.DATASOURCE_C,dataSourceC());

dynamicDataSource.setTargetDataSources(dataSourcesType);

return dynamicDataSource;
}

5.在DAOImpl中切换数据源 或者配置aop切面根据不同的包下面的数据,采用不同的数据源



1,在Dao层直接切换数据源,如切换到默认数据源
DataSourceTypeManager.set(DataSources.DATASOURCE_DEFAULT)

2.根据aop进行动态切换
aop xml配置
<bean id="dataSourceInterceptor" class="com.hxqc.basic.dependency.interceptor.DataSourceInterceptor"/>
<aop:config>
<aop:aspect id="dataSourceAspect" ref="dataSourceInterceptor">
<aop:pointcut id="datasource" expression="execution(* com.hxqc.data.gather.core.erp.*.mapper..*.*(..))"/>
<aop:pointcut id="datasource_oracle" expression="execution(* com.hxqc.data.gather.core.oracle.*.mapper..*.*(..))"/>
<aop:pointcut id="datasource_default" expression="execution(* com.hxqc.data.gather.core.nc.*.mapper..*.*(..))"/>
<aop:before method="setDataSource" pointcut-ref="datasource"/>
<aop:before method="setDataSourceDefault" pointcut-ref="datasource_default"/>
<aop:before method="setDataSourceOracle" pointcut-ref="datasource_oracle"/>
</aop:aspect>
</aop:config>
aop java 配置
@Aspect
@Component
public class DataSourceAspect {

private Logger log = LoggerFactory.getLogger(getClass());

@Pointcut("execution(* com.may.dataA.mapper.*.*(..))")
public void pointCutA(){}

@Pointcut("execution(* com.may.dataB.mapper.*.*(..))")
public void pointCutB(){}

@Pointcut("execution(* com.may.dataC.mapper.*.*(..))")
public void pointCutC(){}
@Pointcut("execution(* com.may..*.*(..))")
public void annotation(){}



public void setDataSourceDefault(JoinPoint jp){
DataSourceManager.set(DataSources.DATASOURCE_A);
log.info("切换数据源","切换到默认数据源A");

}
public void setDataSourceB(JoinPoint jp){
DataSourceManager.set(DataSources.DATASOURCE_B);
log.info("切换数据源","切换到数据源B");
}

public void setDataSourceC(JoinPoint jp){
DataSourceManager.set(DataSources.DATASOURCE_C);
log.info("切换数据源","切换到数据源C");
}

@Before("@annotation(ds)")
public void setAnnotationDataSource(JoinPoint jp,DataSource ds){
// 获取当前的指定的数据源;
String dsId = ds.value();
// 如果不在我们注入的所有的数据源范围之内,那么输出警告信息,系统自动使用默认的数据源。
/*if (!DataSourceManager.containsDataSource(dsId)) {

log.error("数据源[{}]不存在,使用默认数据源 > {}", ds.value(), jp.getSignature());

} else {

log.debug("Use DataSource : {} > {}", ds.value(), jp.getSignature());

// 找到的话,那么设置到动态数据源上下文中。
DynamicDataSourceContextHolder.setDataSourceType(ds.name());
}*/

//将string值转换为enum
DataSourceManager.set(Enum.valueOf(DataSources.class,ds.value()));

log.info("切换数据源"+ds.value(),"切换到默认数据源"+ds.value());
}



}

解释,这里定义三个切面,分别对应三个数据源,如在oracle包下的,动态切换到oracle数据源,上面配置的最后部分是用到了自定义注解DataSource注解,该注解在方法上,通过切面来进行数据源的切换,具体看源码

然后配置切面类
/**
*
*
* @author maple
*
*/
public class DataSourceInterceptor {

public void setDataSourceDefault(JoinPoint jp){
DataSourceTypeManager.set(DataSources.DATASOURCE_DEFAULT);
LogU.i("切换数据源","切换到NC数据源");
System.out.println(jp.getTarget().getClass().getSimpleName());
}

public void setDataSource(JoinPoint jp){
DataSourceTypeManager.set(DataSources.DATASOURCE);
LogU.i("切换数据源","切换到ERP数据源");
}

public void setDataSourceOracle(JoinPoint jp){
DataSourceTypeManager.set(DataSources.DATASOURCE_ORACLE);
LogU.i("切换数据源","切换到Oracle数据源");
}
}





`问题:`多数据源切换是成功了,但牵涉到事务呢?单数据源事务是ok的,但如果多数据源需要同时使用一个事务呢?这个问题有点头大,网络上有人提出用atomikos开源项目实现JTA分布式事务处理。你怎么看?

注意:

本文源码放在码云上,注意,源码将XML配置的内容都改成了JAVA配置,项目是基于Spring-boot,也提供了自定义注解来改变动态数据源!

本文源码:http://git.oschina.net/youjie1/may-dynamic-datasource
原文

 

0 个评论

要回复文章请先登录注册