Spring代码中动态切换数据源
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring代码中动态切换数据源相关的知识,希望对你有一定的参考价值。
在Spring-Mybatis中,有这样一个类AbstractRoutingDataSource,根据名字可以猜到,这是一个框架提供的用于动态选择数据源的类。这个类有两个重要的参数,分别叫
defaultTargetDataSource和targetDataSources。一般的工程都是一个数据源,所以不太接触到这个类。
[html]
<bean id="myoneDataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.myone.driver}"/>
<property name="url" value="${jdbc.myone.url}"/>
<property name="username" value="${jdbc.myone.username}"/>
<property name="password" value="${jdbc.myone.password}"/>
</bean>
<bean id="mytwoDataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.mytwo.driver}"/>
<property name="url" value="${jdbc.mytwo.url}"/>
<property name="username" value="${jdbc.mytwo.username}"/>
<property name="password" value="${jdbc.mytwo.password}"/>
</bean>
<bean id="multipleDataSource" class="dal.datasourceswitch.MultipleDataSource">
<property name="defaultTargetDataSource" ref="myoneDataSource"/> <!--默认主库-->
<property name="targetDataSources">
<map>
<entry key="myone" value-ref="myoneDataSource"/> <!--辅助aop完成自动数据库切换-->
<entry key="mytwo" value-ref="mytwoDataSource"/>
</map>
</property>
</bean>
上面的配置文件对这两个参数的描述已经很清楚了,但这是多个数据源已经确定的场景。我们这篇博客中的场景是多个数据源的信息存在于数据库中,可能数据库中的数据源信息会动态的增加或者减少。这样的话,就不能像上面这样配置了。那怎么办呢?
我们仅仅需要设定默认的数据源,即defaultDataSource参数,至于targetDataSources参数我们需要在代码下载中动态的设定。来看下具体的xml配置:
[html]
<bean id="defaultDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"
p:driverClassName="${db_driver}"
p:url="${db_url}"
p:username="${db_user}"
p:password="${db_pass}"
p:validationQuery="select 1"
p:testOnBorrow="true"/>
<!--动态数据源相关-->
<bean id="dynamicDataSource" class="org.xyz.test.service.datasourceswitch.impl.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="defaultDataSource" value-ref="defaultDataSource"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="defaultDataSource"/>
</bean>
从上面的配置文件中可以看到,我们仅仅配置了默认的数据源defaultDataSource。至于其他的数据源targetDataSources,我们没有配置,需要在代码中动态的创建。关于配置就讲清楚啦!但我们注意到,支持动态数据源的不应该是AbstractRoutingDataSource类吗?怎么上面的配置中是DynamicDataSource类。没错,这个是我们自定义的继承自AbstractRoutingDataSource类的类,也只最重要的类,来看下:(理解这个类,你需要熟练掌握JAVA反射,以及ThreadLocal变量,和Spring的注入机制。别退缩,大家都是这样一步步学过来的!)(下面仅仅是看下全貌,代码的下面会有详细的说明)
[java]
final class DynamicDataSource extends AbstractRoutingDataSource implements ApplicationContextAware{
private static final String DATA_SOURCES_NAME = "targetDataSources";
private ApplicationContext applicationContext;
@Override
protected Object determineCurrentLookupKey() {
DataSourceBeanBuilder dataSourceBeanBuilder = DataSourceHolder.getDataSource();
System.out.println("----determineCurrentLookupKey---"+dataSourceBeanBuilder);
if (dataSourceBeanBuilder == null) {
return null;
}
DataSourceBean dataSourceBean = new DataSourceBean(dataSourceBeanBuilder);
//查看当前容器中是否存在
try {
if (!getTargetDataSources().keySet().contains(dataSourceBean.getBeanName())) {
addNewDataSourceToTargerDataSources(dataSourceBean);
}
return dataSourceBean.getBeanName();
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new SystemException(ErrorEnum.MULTI_DATASOURCE_SWITCH_EXCEPTION);
}
}
private void addNewDataSourceToTargerDataSources(DataSourceBean dataSourceBean) throws NoSuchFieldException, IllegalAccessException {
getTargetDataSources().put(dataSourceBean.getBeanName(), createDataSource(dataSourceBean));
super.afterPropertiesSet();//通知spring有bean更新
}
private Object createDataSource(DataSourceBean dataSourceBean) throws IllegalAccessException {
//在spring容器中创建并且声明bean
ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(BasicDataSource.class);
//将dataSourceBean中的属性值赋给目标bean
Map<String, Object> properties = getPropertyKeyValues(DataSourceBean.class, dataSourceBean);
for (Map.Entry<String, Object> entry : properties.entrySet()) {
beanDefinitionBuilder.addPropertyValue((String) entry.getKey(), entry.getValue());
}
beanFactory.registerBeanDefinition(dataSourceBean.getBeanName(), beanDefinitionBuilder.getBeanDefinition());
return applicationContext.getBean(dataSourceBean.getBeanName());
}
private Map<Object, Object> getTargetDataSources() throws NoSuchFieldException, IllegalAccessException {
Field field = AbstractRoutingDataSource.class.getDeclaredField(DATA_SOURCES_NAME);
field.setAccessible(true);
return (Map<Object, Object>) field.get(this);
}
private <T> Map<String, Object> getPropertyKeyValues(Class<T> clazz, Object object) throws IllegalAccessException {
Field[] fields = clazz.getDeclaredFields();
Map<String, Object> result = new HashMap<>();
for (Field field : fields) {
field.setAccessible(true);
result.put(field.getName(), field.get(object));
}
result.remove("beanName");
return result;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
}
}
首先来看覆盖方法determineCurrentLookupKey(),框架在每次调用数据源时会先调用这个方法,以便知道使用哪个数据源。在本文的场景中,数据源是由程序员在即将切换数据源之前,将要使用的那个数据源的名称放到当前线程的ThreadLocal中,这样在determineCurrentLookupKey()方法中就可以从ThreadLocal中拿到当前请求钥匙用的数据源,从而进行初始化数据源并返回该数据源的操作。在ThreadLocal变量中,我们保存了一个DataSourceBuilder,这是一个建造者模式。读者直接把他理解为是一个数据源的描述就好。因此,determineCurrentLookupKey()方法的流程就是下载:先从ThreadLocal中拿出要使用的数据源信息,然后看当前的targetDataSources中是否有了这个数据源。如果有直接返回。如果没有,创建一个这样的数据源,放到targetDataSources中然后返回。
由于targetDataSources是父类AbstractRoutingDataSource中的一个私有域,因此想要获得他的实例只能通过反射机制。这也是下面的方法存在的意义!
[java]
private Map<Object, Object> getTargetDataSources() throws NoSuchFieldException, IllegalAccessException {
Field field = AbstractRoutingDataSource.class.getDeclaredField(DATA_SOURCES_NAME);
field.setAccessible(true);
return (Map<Object, Object>) field.get(this);
}
然后,我们来看具体是怎么创建下载数据源的。
[java]
private Object createDataSource(DataSourceBean dataSourceBean) throws IllegalAccessException {
//在spring容器中创建并且声明bean
ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(BasicDataSource.class);
//将dataSourceBean中的属性值赋给目标bean
Map<String, Object> properties = getPropertyKeyValues(DataSourceBean.class, dataSourceBean);
for (Map.Entry<String, Object> entry : properties.entrySet()) {
beanDefinitionBuilder.addPropertyValue((String) entry.getKey(), entry.getValue());
}
beanFactory.registerBeanDefinition(dataSourceBean.getBeanName(), beanDefinitionBuilder.getBeanDefinition());
return applicationContext.getBean(dataSourceBean.getBeanName());
}
大家知道,Spring最主要的功能是作为bean容器,即他负责bean生命周期的管理。因此,我们自定义的datasource也不能“逍遥法外”,必须交给Spring容器来管理。这也正是DynamicDataSource类需要实现ApplicationContextAware并且注入ApplicationContext的原因。上面的代码就是根据指定的信息创建一个数据源。这种创建是Spring容器级别的创建。创建完毕之后,需要把刚刚创建的这个数据源放到targetDataSources中,并且还要通知Spring容器,targetDataSources对象变了。下面的方法就是在做这样的事情:
[java]
private void addNewDataSourceToTargerDataSources(DataSourceBean dataSourceBean) throws NoSuchFieldException, IllegalAccessException {
getTargetDataSources().put(dataSourceBean.getBeanName(), createDataSource(dataSourceBean));
super.afterPropertiesSet();//通知spring有bean更新
}
上面的这一步很重要。没有这一步的话,Spring压根就不会知道targetDataSources中多了一个数据源。至此DynamicDataSource类就讲完了。其实仔细想想,思路还是很清晰的。啃掉了DynamicDataSource类这块硬骨头,下面就是一些辅助类了。比如说DataSourceHolder,业务代码通过使用这个类来通知DynamicDataSource中的determineCurrentLookupKey()方法到底使用那个数据源下载:
[java]
public final class DataSourceHolder {
private static ThreadLocal<DataSourceBeanBuilder> threadLocal=new ThreadLocal<DataSourceBeanBuilder>(){
@Override
protected DataSourceBeanBuilder initialValue() {
return null;
}
};
static DataSourceBeanBuilder getDataSource(){
return threadLocal.get();
}
public static void setDataSource(DataSourceBeanBuilder dataSourceBeanBuilder){
threadLocal.set(dataSourceBeanBuilder);
}
public static void clearDataSource(){
threadLocal.remove();
}
}
以上是关于Spring代码中动态切换数据源的主要内容,如果未能解决你的问题,请参考以下文章