Spring中Bean动态加载实现多数据源路由

1、  背景

之前做过一个数据迁移的项目,简单来说就是将一个数据库里面的数据迁移到另外一个数据库。这样的应用必然会涉及到多个数据源连接的问题,并且还要保证系统运行过程中数据源能够随意切换,查询想要的数据。想要达到这个目的其实也不难,我们可以直接使用jdbc连接数据库,在需要使用什么数据源的时候就直接获取对应的连接,并进行后续操作。但是这种方法有两个原因导致很多人不愿意使用:1,需要自己写相应的事务控制代码;2,一般系统都是使用mybatis框架做数据库操作,这样会导致系统代码风格不统一。所以,今天我要介绍的方法是基于Spring+Mybatis框架的多数据源处理。

2、  Spring数据源路由

Spring2.0后增加一个AbstractRoutingDataSource类用来做数据源路由,实现数据源切换的功能就是自定义一个类扩展AbstractRoutingDataSource抽象类,通过重写抽象类中的方法determineCurrentLookupKey()来确定具体的数据源,具体实现代码如下:

1 public class DynamicDataSource extends AbstractRoutingDataSource {
2     @Resource(name = "dynamicDataSourceSelector")
3     private DataSourceSelector dynamicDataSourceSelector;
4
5     @Override
6     protected Object determineCurrentLookupKey() {
7         return dynamicDataSourceSelector.getRouteKey();
8     }
9 }

通过自定义的一个DataSourceSelector来设置需要路由的数据源Key,实现代码如下(选择过程可以按照需求自行变换):

 1 public class DataSourceSelector {
 2
 3    private static ThreadLocal<String> localRouteKey = new ThreadLocal<>();
 4    public void setRouteKey(String routeKey){
 5       localRouteKey.set(routeKey);
 6    }
 7
 8    public String getRouteKey(){
 9       return localRouteKey.get();
10    }
11
12 }

在xml文件中配置多个数据源:

 1 <!-- 配置数据源 -->
 2 <!-- 数据源1 -->
 3 <bean id="dynamicBaseDataSource1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
 4     <property name="url" value="jdbc:mysql://localhost:3306?useUnicode=true&amp;characterEncoding=UTF-8"/>
 5     <property name="username" value="root"/>
 6     <property name="password" value="root"/>
 7 </bean>
 8 <!-- 数据源2 -->
 9 <bean id="dynamicBaseDataSource2" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
10     <property name="url" value="jdbc:mysql://112.74.223.43:3306?useUnicode=true&amp;characterEncoding=UTF-8"/>
11     <property name="username" value="root"/>
12     <property name="password" value="******"/>
13 </bean>
14 <!-- 数据源3 -->
15 <bean id="dynamicBaseDataSource3" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
16     <property name="url" value="jdbc:mysql://21.123.45.14:3306?useUnicode=true&amp;characterEncoding=UTF-8"/>
17     <property name="username" value="root"/>
18     <property name="password" value="******"/>
19 </bean>

还需要配置多个数据源对应的Key的映射关系:

 1 <bean id="dynamicDataSource" class="com.guigui.datasource.DynamicDataSource">
 2     <property name="targetDataSources">
 3         <map>
 4             <!-- 多个数据源Key-value列表 -->
 5             <entry key="dynamicDS1" value-ref="dynamicBaseDataSource1"/>
 6             <entry key="dynamicDS2" value-ref="dynamicBaseDataSource2"/>
 7             <entry key="dynamicDS3" value-ref="dynamicBaseDataSource3"/>
 8         </map>
 9     </property>
10 </bean>

SessionFactory以及事务等配置如下:

 1 <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
 2     <property name="basePackage" value="com.guigui.dynamic.dao"/>
 3     <property name="sqlSessionFactoryBeanName" value="dynamicSqlSessionFactory"/>
 4 </bean>
 5
 6 <bean id="dynamicDataSourceSelector" class="com.guigui.datasource.DataSourceSelector" />
 7
 8 <!-- 事务管理相关配置... -->
 9 <bean id="dynamicTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
10     <property name="dataSource" ref="dynamicDataSource"/>
11 </bean>
12
13 <aop:config>
14     <aop:pointcut id="dynamicTxOperation" expression="execution(* com.guigui.dynamic.service.*Service.*(..))" />
15     <aop:advisor id="dynamicAdvisor" pointcut-ref="dynamicTxOperation" advice-ref="dynamicAdvice"/>
16 </aop:config>
17
18 <tx:advice id="dynamicAdvice" transaction-manager="dynamicTransactionManager">
19     <tx:attributes>
20         <tx:method name="*InTrx" propagation="REQUIRED" />
21         <tx:method name="*InNewTrx" propagation="REQUIRES_NEW" />
22         <tx:method name="*NoTrx" propagation="NOT_SUPPORTED" />
23         <tx:method name="*" propagation="SUPPORTS" />
24     </tx:attributes>
25 </tx:advice>

配置好以后就可以使用多数据源切换的功能了,通过DataSourceSelector中的setRouteKey()方法进行数据源切换,切换之后对数据库的操作就是当前数据源的了。

这种方法相对于直接通过jdbc连接的方式确实方便了许多,直接使用了Spring框架提供的事务支持,对数据库的操作也可以用Mybatis框架来做。But!!  这种方式也会存在一些让人不是很爽的地方,细心的同学们可能已经发现了,那就是我们的多个数据源都是配置在Spring的xml配置文件里面的,这就导致了我们每次新增加一个数据源都得修改一次xml文件,并且进行一次版本发布,想想就很不爽啊~~~ 而且,随着如果系统中连接的数据源越来越多,我们的配置文件也会越来越长,代码也会很难看!那么能不能把这些变化的数据源信息做成配置的呢?虽然不是很容易,但是方法还是有的,这就是今天的主题:动态注入。

3、  Spring动态注入Bean

由于Spring传统的注入Bean的方式是通过加载xml配置文件来依次注入配置文件中定义的Bean,如果数据源的Bean通过其他方式配置,就需要在代码中进行动态注入。数据源的配置方式可以是任意方式,只要能够在代码中读取到即可,本文通过从数据库中读取数据源配置内容来实现多数据源路由。

动态注入步骤:

  1. 从数据库中读取数据源配置列表,遍历数据源配置列表,并且对每条配置单独进行处理;
  2. 每条配置均需构造一个数据源的Bean并注入到Spring容器:
    1 <!-- 配置数据源 -->
    2 <!-- 其他多个数据源配置从配置表中读取,并在应用启动时进行加载(动态注入Spring容器) -->
    3 <bean id="dynamicBaseDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    4     <property name="url" value="jdbc:mysql://localhost:3306?useUnicode=true&amp;characterEncoding=UTF-8"/>
    5     <property name="username" value="root"/>
    6     <property name="password" value="root"/>
    7 </bean>
  3. 需要将新构造的数据源Bean加到动态数据源的targetDataSources这个Map结构的属性中,并将动态数据源Bean重新注册:
    1 <!-- 其他多个数据源配置从配置表中读取,并在应用启动时进行加载(动态注入Spring容器) -->
    2 <entry key="defaultDS" value-ref="dynamicBaseDataSource"/>
  4. 由于事务管理相关配置依赖了原有的动态数据源,而动态数据源已经更新,所以相应的事务管理配置也要更新;同样的,事务相关的拦截器advisor、advice由于依赖事务管理器也都需要更新。

    数据源动态注入代码:

     1 public class DynamicInjectDataSource {
     2
     3     @Autowired
     4     private DatasourceConfigMapper datasourceConfigMapper;
     5
     6     private static final String URL_PREFIX = "jdbc:mysql://";
     7     private static final String URL_SURFIX = "?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull";
     8     private static final String DESTORY_METHOD = "close";
     9     private static final String DYNAMIC_DATASOURCE = "dynamicDataSource";
    10
    11     public void startUp() throws Exception {
    12         this.dynamicInject();
    13     }
    14
    15     private void dynamicInject() throws Exception {
    16         ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) SpringContextHolder.getContext();
    17         DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();
    18         ManagedMap<String, BeanDefinition> dataSourceMap = new ManagedMap<>();
    19         List<DatasourceConfig> dataSourceConfigList = datasourceConfigMapper.selectAllDataSource();
    20         if (CollectionUtils.isEmpty(dataSourceConfigList)) {
    21             System.out.println("未查询到相关数据源!");
    22             throw new Exception("初始化动态数据源失败!");
    23         }
    24         for (DatasourceConfig config : dataSourceConfigList) {
    25             String beanId = config.getBeanId();
    26             System.out.println("开始注册Mysql数据源:" + config.getDsKey());
    27             // 如果存在则需要重新注册,防止有修改需要刷新
    28             if (defaultListableBeanFactory.containsBean(beanId)) {
    29                 defaultListableBeanFactory.removeBeanDefinition(beanId);
    30             }
    31             // 注册新的Bean
    32             BeanDefinitionBuilder dataSourceBuilder = BeanDefinitionBuilder.genericBeanDefinition(BasicDataSource.class);
    33             dataSourceBuilder.setDestroyMethodName(DESTORY_METHOD);
    34             dataSourceBuilder.addPropertyValue("url", URL_PREFIX + config.getUrl() + URL_SURFIX);
    35             dataSourceBuilder.addPropertyValue("username", config.getUserName());
    36             dataSourceBuilder.addPropertyValue("password", config.getPassword());
    37             dataSourceBuilder.addPropertyValue("maxActive", config.getMaxactive());
    38             defaultListableBeanFactory.registerBeanDefinition(beanId, dataSourceBuilder.getRawBeanDefinition());
    39             // 动态添加数据源
    40             dataSourceMap.put(config.getDsKey(), dataSourceBuilder.getRawBeanDefinition());
    41         }
    42
    43         /* 重新注册动态数据源**/
    44         Map<String, Object> dynamicDSPropertiesMap = new HashMap<>();
    45         dynamicDSPropertiesMap.put("targetDataSources", dataSourceMap);
    46         BeanDefinition dynamicDataSourceBean = this.reRegisterBeanDefinition(DYNAMIC_DATASOURCE, dynamicDSPropertiesMap);
    47
    48         /* 重新注册事务管理器**/
    49         Map<String, Object> dynamicDSManagerProsMap = new HashMap<>();
    50         dynamicDSManagerProsMap.put("dataSource", dynamicDataSourceBean);
    51         BeanDefinition dynamicManageBean = this.reRegisterBeanDefinition("dynamicTransactionManager", dynamicDSManagerProsMap);
    52
    53         /* 重新注册Advice**/
    54         Map<String, Object> dynamicAdviceProsMap = new HashMap<>();
    55         dynamicAdviceProsMap.put("transactionManager", dynamicManageBean);
    56         this.reRegisterBeanDefinition("dynamicAdvice", dynamicAdviceProsMap);
    57
    58         /* 重新注册Advisor**/
    59         Map<String, Object> dynamicAdvisorProsMap = new HashMap<>();
    60         dynamicAdvisorProsMap.put("adviceBeanName", "dynamicAdvice");
    61         this.reRegisterBeanDefinition("dynamicAdvisor", dynamicAdvisorProsMap);
    62
    63     }
    64
    65     /**
    66      * 重新注册Bean通用方法
    67      *
    68      * @param beanName   bean名称
    69      * @param properties 属性
    70      */
    71     private BeanDefinition reRegisterBeanDefinition(String beanName, Map<String, Object> properties) {
    72         ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) SpringContextHolder.getContext();
    73         DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();
    74         BeanDefinition regBean = defaultListableBeanFactory.getBeanDefinition(beanName);
    75         Set<String> propertyKeys = properties.keySet();
    76         // 重新设置Bean的属性
    77         for (String propertyKey : propertyKeys) {
    78             regBean.getPropertyValues().removePropertyValue(propertyKey);
    79             regBean.getPropertyValues().add(propertyKey, properties.get(propertyKey));
    80         }
    81         // 删除原有Bean
    82         if (defaultListableBeanFactory.containsBean(beanName)) {
    83             defaultListableBeanFactory.removeBeanDefinition(beanName);
    84         }
    85         // 重新注册Bean
    86         defaultListableBeanFactory.registerBeanDefinition(beanName, regBean);
    87         return regBean;
    88     }
    89 }

其中存储数据源配置的表结构如下:

4、基于配置的动态数据源路由测试

在数据库中我配置了两个数据源,一个是我本地创建的数据库,另外一个是我VPS上部署的数据库。

在应用启动的时候会将这两个数据源加载到Spring容器,并且可以通过ds_key来路由具体的数据源。测试程序分别打印出两个数据源的数据库里面的一张表的字段列表。

以下是具体测试代码:

 1 @Service("dynamicServiceImpl")
 2 public class DynamicServiceImpl implements IDynamicService {
 3     @Resource(name = "dynamicDataSourceSelector")
 4     private DataSourceSelector dynamicDataSourceSelector;
 5     @Autowired
 6     private DynamicMapper dynamicMapper;
 7     @Override
 8     public void dynamicRouting(String routingKey, String tableName, String schema) {
 9         // 路由数据源
10         System.out.println("路由到数据源:" + routingKey);
11         dynamicDataSourceSelector.setRouteKey(routingKey);
12         // 从当前数据源中进行查找
13         System.out.println("显示数据源 " + routingKey + "的表: " + schema + "." + tableName + " 字段列表:");
14         List<String> colnums = dynamicMapper.selectAllColumns(schema, tableName);
15         // 打印字段列表
16         StringBuilder sb = new StringBuilder();
17         sb.append("[");
18         for (int i = 0; i < colnums.size(); i++) {
19             sb.append(colnums.get(i)).append(",");
20             if (i == colnums.size() - 1) {
21                 sb.delete(sb.length() - 1, sb.length());
22                 sb.append("]");
23             }
24         }
25         System.out.println(sb.toString());
26         System.out.println();
27     }
28
29 }
1 @Test
2 public void testDynamicSource() {
3     // 路由DSVps数据源
4     dynamicServiceImpl.dynamicRouting("DSVps", "article", "myblog");
5
6     // 路由DSLocal数据源
7     dynamicServiceImpl.dynamicRouting("DSLocal", "khmessage", "weiyaqi");
8 }

测试结果如下:

通过上面测试结果我们可以看到,在Spring的xml配置中不需要配置这些数据源,我们也做到了在这些数据源之间来回切换,而且数据源的个数我们也可以任意增加(只需要在数据库表中添加一条配置的记录即可),而我们的xml配置却依旧保持不变并且很简洁,配置一个默认的数据源,其他的都通过数据库配置读取并且动态注入:

 1 <!-- 配置数据源 -->
 2 <!-- 其他多个数据源配置从配置表中读取,并在应用启动时进行加载(动态注入Spring容器) -->
 3 <bean id="dynamicBaseDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
 4     <property name="url" value="jdbc:mysql://localhost:3306?useUnicode=true&amp;characterEncoding=UTF-8"/>
 5     <property name="username" value="root"/>
 6     <property name="password" value="root"/>
 7 </bean>
 8
 9 <!-- 配置数据源路由,targetDataSources.key作为数据源唯一标识 -->
10 <bean id="dynamicDataSource" class="com.guigui.datasource.DynamicDataSource">
11     <property name="targetDataSources">
12         <map>
13             <!-- 其他多个数据源配置从配置表中读取,并在应用启动时进行加载(动态注入Spring容器) -->
14             <entry key="defaultDS" value-ref="dynamicBaseDataSource"/>
15         </map>
16     </property>
17 </bean>

新增了数据源后,由于配置和应用是分开的,也不需要重新发布应用了。如果想更进一步不重启应用就能达到刷新数据源的目的,可以通过其他方式如定时任务或者页面调用等方式触发DynamicInjectDataSource. startUp()方法来完成数据源刷新。

以上便是本次要介绍的全部内容,如果有什么问题,欢迎各位读者指正,感激不尽!

动态数据源路由demo源码已上传至GitHub: https://github.com/guishenyouhuo/dynamicdatasource

原文地址:https://www.cnblogs.com/guishenyouhuo/p/9956099.html

时间: 2024-08-08 11:52:47

Spring中Bean动态加载实现多数据源路由的相关文章

【Spring】详解Spring中Bean的加载

之前写过bean的解析,这篇来讲讲bean的加载,加载要比bean的解析复杂些,该文之前在小编原文中有发表过,要看原文的可以直接点击原文查看,从之前的例子开始,Spring中加载一个bean的方式: TestBean bean = factory.getBean("testBean"); 来看看getBean(String name)方法源码, @Override public Object getBean(String name) throws BeansException { re

Spring中资源的加载ResourceLoader

Spring中资源的加载是定义在ResourceLoader接口中的,它跟前面提到的抽象资源的关系如下: ResourceLoader的源码 [java] view plain copy public interface ResourceLoader { /** Pseudo URL prefix for loading from the class path: "classpath:" */ String CLASSPATH_URL_PREFIX = ResourceUtils.CL

解决tableView中cell动态加载控件的重用问题

tableView的cell,有时候需要在运行时取得对应的数据后才能够动态的创建该cell中的控件并加载到该cell中,此时,你一定会遇到重用问题,即使你能做到该cell只根据数值加载了一回控件,你也没法保证不出现重用问题:) 效果(请注意查看,移动下面的格子时,上面出现了重用的问题) 源码: YXCell.h // // YXCell.h // YXTableView // // Copyright (c) 2014年 Y.X. All rights reserved. // #import

Android中apk动态加载技术研究(2)android插件化及实现

了解了android中类加载的前期知识点后,来看看android中DexClassLoader具体的实现 具体加载流程如下: 宿主程序会到文件系统比如SD卡中去加载APK[1],然后通过一个叫proxy的Activity去执行apk中的Activity 关于动态加载ap,理论上可用用到DexClassLoad.PathClassLoader.URLClassLoader; DexClassLoader: 可以加载文件系统上的jar.dex.apk PathClassLoader:可以加载 /da

Android中的动态加载机制

在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势.本文对网上Android动态加载jar的资料进行梳理和实践在这里与大家一起分享,试图改善频繁升级这一弊病. Android应用开发在一般情况下,常规的开发方式和代码架构就能满足我们的普通需求.但是有些特殊问题,常常引发我们进一步的沉思.我们从沉思中产生顿悟,从而产生新的技术形式.如何开发一个可以自定义

Android中的动态加载机制--薛彦顺

在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势.本文对网上Android动态加载jar的资料进行梳理和实践在这里与大家一起分享,试图改善频繁升级这一弊病. Android应用开发在一般情况下,常规的开发方式和代码架构就能满足我们的普通需求.但是有些特殊问题,常常引发我们进一步的沉思.我们从沉思中产生顿悟,从而产生新的技术形式. 如何开发一个可以自定

bootstrap中的动态加载出来的图片轮播中的li标签中的class=&quot;active&quot;的动态添加移除

//该方法是在slide改变时立即触发该事件, $('#myCarousel').on('slide.bs.carousel', function () { $("#myCarousel ol li").toggleClass("active");//重复切换类名“active” }); 遇到的问题:在动态加载出来的轮播中设置了加载时就开始轮播data-ride="carousel"图片可以轮播但是底下的li标签的class没有进行切换.而不加d

Android中apk动态加载技术研究(1)基础知识研修

java classloader 和android中DexClassloader对比: Java ClassLoader : 作用: 主要用来加载class 到jvm中,以供程序使用,也就是说:java程序可以动态加载类定义,而这个动态加载机制就是通过ClassLoader来实现的 核心loader: A:: bootstrap classloader(启动类加载器) -->加载java核心api,包括用户自定义的classloader和另外两个loader B: ExtClassLoader

关于Unity3D中Resources动态加载NGUI图片的方法

在NGUI中有些图片我需要动态进行变更或者加载,怎么办? 首先在项目中创建一个Resources目录,接着把需要的图片放在这里面,可以有子文件夹么?当然可以,文件结构很重要哦~ NGUI加载图片的方法其实是加载NGUI生成的atlas,大家可以看看NGUI的图集文件(一个material.一个prefab,一张图集),我们要做的就是动态加载这个prefab(它有UIAtlas属性),然后通过图片名称更改图片. 我这里那UISprite来说明,我是这样做的: UIAtlas tu = Resour