如何在Spring代码中动态切换数据源

在Spring-Mybatis中,有这样一个类AbstractRoutingDataSource,根据名字可以猜到,这是一个框架提供的用于动态选择数据源的类。这个类有两个重要的参数,分别叫

defaultTargetDataSource和targetDataSources。一般的工程都是一个数据源,所以不太接触到这个类。

[html]  view plain  copy
  1. <bean id="myoneDataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">    
  2.     <property name="driverClassName" value="${jdbc.myone.driver}"/>    
  3.     <property name="url" value="${jdbc.myone.url}"/>    
  4.     <property name="username" value="${jdbc.myone.username}"/>    
  5.     <property name="password" value="${jdbc.myone.password}"/>    
  6. </bean>    下载
  7. <bean id="mytwoDataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">    
  8.     <property name="driverClassName" value="${jdbc.mytwo.driver}"/>    
  9.     <property name="url" value="${jdbc.mytwo.url}"/>    
  10.     <property name="username" value="${jdbc.mytwo.username}"/>    
  11.     <property name="password" value="${jdbc.mytwo.password}"/>    
  12. </bean>    
  13.   
  14. <bean id="multipleDataSource" class="dal.datasourceswitch.MultipleDataSource">    
  15.     <property name="defaultTargetDataSource" ref="myoneDataSource"/> <!--默认主库-->    
  16.     <property name="targetDataSources">    
  17.         <map>    
  18.             <entry key="myone" value-ref="myoneDataSource"/>            <!--辅助aop完成自动数据库切换-->    
  19.             <entry key="mytwo" value-ref="mytwoDataSource"/>    
  20.         </map>    
  21.     </property>    
  22. </bean>   

上面的配置文件对这两个参数的描述已经很清楚了,但这是多个数据源已经确定的场景。我们这篇博客中的场景是多个数据源的信息存在于数据库中,可能数据库中的数据源信息会动态的增加或者减少。这样的话,就不能像上面这样配置了。那怎么办呢?

我们仅仅需要设定默认的数据源,即defaultDataSource参数,至于targetDataSources参数我们需要在代码中下载动态的设定。来看下具体的xml配置:

[html]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <bean id="defaultDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"  
  2.           p:driverClassName="${db_driver}"  
  3.           p:url="${db_url}"  
  4.           p:username="${db_user}"  
  5.           p:password="${db_pass}"  
  6.           p:validationQuery="select 1"  
  7.           p:testOnBorrow="true"/>  
  8.   
  9.     <!--动态数据源相关-->  
  10.     <bean id="dynamicDataSource" class="org.xyz.test.service.datasourceswitch.impl.DynamicDataSource">  
  11.         <property name="targetDataSources">  
  12.             <map key-type="java.lang.String">  
  13.                 <entry key="defaultDataSource" value-ref="defaultDataSource"/>  
  14.             </map>  
  15.         </property>  
  16.         <property name="defaultTargetDataSource" ref="defaultDataSource"/>  
  17.     </bean>  

从上面的配置文件中可以看到,我们仅仅配置了默认的数据源defaultDataSource。至于其他的数据源targetDataSources,我们没有配置,需要在代码中动态的创建。

[java]  view plain  copy
 
  1. final class DynamicDataSource extends AbstractRoutingDataSource implements ApplicationContextAware{  
  2.   
  3.     private static final String DATA_SOURCES_NAME = "targetDataSources";  
  4.   
  5.     private ApplicationContext applicationContext;  
  6.   
  7.     @Override  
  8.     protected Object determineCurrentLookupKey() {  
  9.         DataSourceBeanBuilder dataSourceBeanBuilder = DataSourceHolder.getDataSource();  
  10.         System.out.println("----determineCurrentLookupKey---"+dataSourceBeanBuilder);  
  11.         if (dataSourceBeanBuilder == null) {  
  12.             return null;  
  13.         }  
  14.         DataSourceBean dataSourceBean = new DataSourceBean(dataSourceBeanBuilder);  
  15.         //查看当前容器中是否存在  
  16.         try {  
  17.             if (!getTargetDataSources().keySet().contains(dataSourceBean.getBeanName())) {  
  18.                 addNewDataSourceToTargerDataSources(dataSourceBean);  
  19.             }  
  20.             return dataSourceBean.getBeanName();  
  21.         } catch (NoSuchFieldException | IllegalAccessException e) {  
  22.             throw new SystemException(ErrorEnum.MULTI_DATASOURCE_SWITCH_EXCEPTION);  
  23.         }  
  24.     }  下载
  25.   
  26.     private void addNewDataSourceToTargerDataSources(DataSourceBean dataSourceBean) throws NoSuchFieldException, IllegalAccessException {  
  27.         getTargetDataSources().put(dataSourceBean.getBeanName(), createDataSource(dataSourceBean));  
  28.         super.afterPropertiesSet();//通知spring有bean更新  
  29.     }  
  30.   
  31.     private Object createDataSource(DataSourceBean dataSourceBean) throws IllegalAccessException {  
  32.         //在spring容器中创建并且声明bean  
  33.         ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;  
  34.         DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();  
  35.         BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(BasicDataSource.class);  
  36.         //将dataSourceBean中的属性值赋给目标bean  
  37.         Map<String, Object> properties = getPropertyKeyValues(DataSourceBean.class, dataSourceBean);  
  38.         for (Map.Entry<String, Object> entry : properties.entrySet()) {  
  39.             beanDefinitionBuilder.addPropertyValue((String) entry.getKey(), entry.getValue());  
  40.         }  
  41.         beanFactory.registerBeanDefinition(dataSourceBean.getBeanName(), beanDefinitionBuilder.getBeanDefinition());  
  42.         return applicationContext.getBean(dataSourceBean.getBeanName());  
  43.     }  
  44.   
  45.     private Map<Object, Object> getTargetDataSources() throws NoSuchFieldException, IllegalAccessException {  
  46.         Field field = AbstractRoutingDataSource.class.getDeclaredField(DATA_SOURCES_NAME);  
  47.         field.setAccessible(true);  
  48.         return (Map<Object, Object>) field.get(this);  
  49.     }  
  50.   
  51.   
  52.     private <T> Map<String, Object> getPropertyKeyValues(Class<T> clazz, Object object) throws IllegalAccessException {  
  53.         Field[] fields = clazz.getDeclaredFields();  
  54.         Map<String, Object> result = new HashMap<>();  
  55.         for (Field field : fields) {  
  56.             field.setAccessible(true);  
  57.             result.put(field.getName(), field.get(object));  
  58.         }  
  59.         result.remove("beanName");  
  60.         return result;  
  61.     }  
  62.   
  63.     @Override  
  64.     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {  
  65.         this.applicationContext=applicationContext;  
  66.     }  
  67. }  


首先来看覆盖方法determineCurrentLookupKey(),框架在每次调用数据源时会先调用这个方法,以便知道使用哪个数据源。在本文的场景中,数据源是由程序员在即将切换数据源之前,将要使用的那个数据源的名称放到当前线程的ThreadLocal中,这样在determineCurrentLookupKey()方法中就可以从ThreadLocal中拿到当前请求钥匙用的数据源,从而进行初始化数据源并返回该数据源的操作。在ThreadLocal变量中,我们保存了一个DataSourceBuilder,这是一个建造者模式。读者直接把他理解为是一个数据源的描述就好。因此,determineCurrentLookupKey()方法的流程就是下载:先从ThreadLocal中拿出要使用的数据源信息,然后看当前的targetDataSources中是否有了这个数据源。如果有直接返回。如果没有,创建一个这样的数据源,放到targetDataSources中然后返回。


由于targetDataSources是父类AbstractRoutingDataSource中的一个私有域,因此想要获得他的实例只能通过反射机制。这也是下面的方法存在的意义!

[java]  view plain  copy
 
  1. private Map<Object, Object> getTargetDataSources() throws NoSuchFieldException, IllegalAccessException {  
  2.      Field field = AbstractRoutingDataSource.class.getDeclaredField(DATA_SOURCES_NAME);  
  3.      field.setAccessible(true);  
  4.      return (Map<Object, Object>) field.get(this);  
  5.  }  


然后,我们来看具体是怎么创建数据源的。

[java]  view plain  copy
 
  1. private Object createDataSource(DataSourceBean dataSourceBean) throws IllegalAccessException {  
  2.        //在spring容器中创建并且声明bean  
  3.        ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;  
  4.        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();  
  5.        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(BasicDataSource.class);  
  6.        //将dataSourceBean中的属性值赋给目标bean  
  7.        Map<String, Object> properties = getPropertyKeyValues(DataSourceBean.class, dataSourceBean);  
  8.        for (Map.Entry<String, Object> entry : properties.entrySet()) {  
  9.            beanDefinitionBuilder.addPropertyValue((String) entry.getKey(), entry.getValue());  
  10.        }  
  11.        beanFactory.registerBeanDefinition(dataSourceBean.getBeanName(), beanDefinitionBuilder.getBeanDefinition());  
  12.        return applicationContext.getBean(dataSourceBean.getBeanName());  
  13.    }  

大家知道,Spring最主要的功能是作为bean容器,即他负责bean生命周期的管理。因此,我们自定义的datasource也不能“逍遥法外”,必须交给Spring容器来管理。这也正是DynamicDataSource类需要实现ApplicationContextAware并且注入ApplicationContext的原因。上面的代码就是根据指定的信息创建一个数据源。这种创建是Spring容器级别的创建。创建完毕之后,需要把刚刚创建的这个数据源放到targetDataSources中,并且还要通知Spring容器,targetDataSources对象变了。下载下面的方法就是在做这样的事情:

[java]  view plain  copy
 
  1. private void addNewDataSourceToTargerDataSources(DataSourceBean dataSourceBean) throws NoSuchFieldException, IllegalAccessException {  
  2.       getTargetDataSources().put(dataSourceBean.getBeanName(), createDataSource(dataSourceBean));  
  3.       super.afterPropertiesSet();//通知spring有bean更新  
  4.   }  
上面的这一步很重要。没有这一步的话,Spring压根就不会知道targetDataSources中多了一个数据源。至此DynamicDataSource类就讲完了。其实仔细想想,思路还是很清晰的。啃掉了DynamicDataSource类这块硬骨头,下面就是一些辅助类了。比如说DataSourceHolder,业务代码通过使用这个类来通知DynamicDataSource中的determineCurrentLookupKey()方法到底使用那个数据源:

[java]  view plain  copy
 
  1. public final class DataSourceHolder {  
  2.     private static ThreadLocal<DataSourceBeanBuilder> threadLocal=new ThreadLocal<DataSourceBeanBuilder>(){  
  3.         @Override  
  4.         protected DataSourceBeanBuilder initialValue() {  
  5.             return null;  
  6.         }  
  7.     };  
  8.   
  9.     static DataSourceBeanBuilder getDataSource(){  
  10.         return threadLocal.get();  
  11.     }  
  12.   
  13.     public static void setDataSource(DataSourceBeanBuilder dataSourceBeanBuilder){  
  14.         threadLocal.set(dataSourceBeanBuilder);  
  15.     }  
  16.   
  17.   
  18.     public static void clearDataSource(){  
  19.         threadLocal.remove();  
  20.     }  
  21. }  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值