Spring代码中动态切换数据源

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring代码中动态切换数据源相关的知识,希望对你有一定的参考价值。

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

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


[html]

  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] 

  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,我们没有配置,需要在代码中动态的创建。关于配置就讲清楚啦!但我们注意到,支持动态数据源的不应该是AbstractRoutingDataSource类吗?怎么上面的配置中是DynamicDataSource类。没错,这个是我们自定义的继承自AbstractRoutingDataSource类的类,也只最重要的类,来看下:(理解这个类,你需要熟练掌握JAVA反射,以及ThreadLocal变量,和Spring的注入机制。别退缩,大家都是这样一步步学过来的!)(下面仅仅是看下全貌,代码的下面会有详细的说明)


[java] 

  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] 

  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]

  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] 

  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] 

  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. }  



以上是关于Spring代码中动态切换数据源的主要内容,如果未能解决你的问题,请参考以下文章

Spring主从数据库的配置和动态数据源切换原理

基于spring的aop实现多数据源动态切换

多个mongoDB数据源,怎么配置动态切换

spring+mybatis多数据源动态切换

Spring动态配置多数据源的基于spring和ibatis的多数据源切换方案

Spring Boot 动态数据源(多数据源自动切换)