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&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&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&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通过其他方式配置,就需要在代码中进行动态注入。数据源的配置方式可以是任意方式,只要能够在代码中读取到即可,本文通过从数据库中读取数据源配置内容来实现多数据源路由。
动态注入步骤:
- 从数据库中读取数据源配置列表,遍历数据源配置列表,并且对每条配置单独进行处理;
-
每条配置均需构造一个数据源的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&characterEncoding=UTF-8"/> 5 <property name="username" value="root"/> 6 <property name="password" value="root"/> 7 </bean>
-
需要将新构造的数据源Bean加到动态数据源的targetDataSources这个Map结构的属性中,并将动态数据源Bean重新注册:
1 <!-- 其他多个数据源配置从配置表中读取,并在应用启动时进行加载(动态注入Spring容器) --> 2 <entry key="defaultDS" value-ref="dynamicBaseDataSource"/>
- 由于事务管理相关配置依赖了原有的动态数据源,而动态数据源已经更新,所以相应的事务管理配置也要更新;同样的,事务相关的拦截器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&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