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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM