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:

    扫描二维码关注公众号,回复: 4065269 查看本文章
    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结构的属性中:

    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

猜你喜欢

转载自www.cnblogs.com/guishenyouhuo/p/9956099.html
今日推荐