Spring boot多数据源实现动态切换
概述
日常的业务开发项目中只会配置一套数据源,如果需要获取其他系统的数据往往是通过调用接口, 或者是通过第三方工具比如kettle将数据同步到自己的数据库中进行访问。
但是也会有需要在项目中引用多数据源的场景。比如如下场景:
- 自研数据迁移系统,至少需要新、老两套数据源,从老库读取数据写入新库
- 自研读写分离中间件,系统流量增加,单库响应效率降低,引入读写分离方案,写入数据是一个数据源,读取数据是另一个数据源
环境说明
- spring boot
- mysql
- mybatis-plus
- spring-aop
项目目录结构
- controller: 存放接口类
- service: 存放服务类
- mapper: 存放操作数据库的mapper接口
- entity: 存放数据库表实体类
- vo: 存放返回给前端的视图类
- context: 存放持有当前线程数据源key类
- constants: 存放定义数据源key常量类
- config: 存放数据源配置类
- annotation: 存放动态数据源注解
- aop: 存放动态数据源注解切面类
- resources.config: 项目配置文件
- resources.mapper: 数据库xml文件
关键类说明
忽略掉controller/service/entity/mapper/xml介绍。
- jdbc.properties: 数据源配置文件。虽然可以配置到Spring boot的默认配置文件application.properties/application.yml文件当中,但是如果数据源比较多的话,根据实际使用,最佳的配置方式还是独立配置比较好。
- DynamicDataSourceConfig: 数据源配置类
- DynamicDataSource: 动态数据源配置类
- DataSourceRouting: 动态数据源注解
- DynamicDataSourceAspect: 动态数据源设置切面
- DynamicDataSourceContextHolder: 当前线程持有的数据源key
- DataSourceConstants: 数据源key常量类
开发流程

动态数据源流程
Spring Boot 的动态数据源,本质上是把多个数据源存储在一个 Map 中,当需要使用某个数据源时,从 Map 中获取此数据源进行处理。

在 Spring 中已提供了抽象类 AbstractRoutingDataSource 来实现此功能,继承AbstractRoutingDataSource类并覆写其determineCurrentLookupKey()方法即可,该方法只需要返回数据源key即可,也就是存放数据源的Map的key。
因此,我们在实现动态数据源的,只需要继承它,实现自己的获取数据源逻辑即可。AbstractRoutingDataSource顶级继承了DataSource,所以它也是可以做为数据源对象,因此项目中使用它作为主数据源。

AbstractRoutingDataSource原理
AbstractRoutingDataSource中有一个重要的属性:

- targetDataSources: 目标数据源,即项目启动的时候设置的需要通过AbstractRoutingDataSource管理的数据源。
- defaultTargetDataSource: 默认数据源,项目启动的时候设置的默认数据源,如果没有指定数据源,默认返回改数据源。
- resolvedDataSources: 也是存放的数据源,是对targetDataSources进行处理后进行存储的。可以看一下源码。

- resolvedDefaultDataSource: 对默认数据源进行了二次处理,源码如上图最后的两行代码。
AbstractRoutingDataSource中所有的方法和属性:

比较重要的是determineTargetDataSource方法。
protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; } /** * Determine the current lookup key. This will typically be * implemented to check a thread-bound transaction context. * <p>Allows for arbitrary keys. The returned key needs * to match the stored lookup key type, as resolved by the * {@link #resolveSpecifiedLookupKey} method. */ @Nullable protected abstract Object determineCurrentLookupKey();
这个方法主要就是返回一个DataSource对象,主要逻辑就是先通过方法determineCurrentLookupKey获取一个Object对象的lookupKey,然后通过这个lookupKey到resolvedDataSources中获取数据源(resolvedDataSources就是一个Map,上面已经提到过了);如果没有找到数据源,就返回默认的数据源。determineCurrentLookupKey就是程序员配置动态数据源需要自己实现的方法。
问题
配置多数据源后启动项目报错:Property 'sqlSessionFactory' or 'sqlSession Template' are required。
翻译过来就是:需要属性“sqlSessionFactory”或“sqlSessionTemplate”。也就是说 注入数据源的时候需要这两个数据,但是这两个属性在启动容器中没有找到。
当引入mybatis-plus依赖mybatis-plus-boot-starter后,会添加一个自动配置类 MybatisPlusAutoConfiguration 。 其中有两个方法sqlSessionFactory()、sqlSessionTemplate()。这两个方法就是 给容器中注入“sqlSessionFactory”或“sqlSessionTemplate”两个属性。
@Configuration( proxyBeanMethods = false ) @ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class}) @ConditionalOnSingleCandidate(DataSource.class) @EnableConfigurationProperties({MybatisPlusProperties.class}) @AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class}) public class MybatisPlusAutoConfiguration implements InitializingBean { @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean(); factory.setDataSource(dataSource); factory.setVfs(SpringBootVFS.class); if (StringUtils.hasText(this.properties.getConfigLocation())) { factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation())); } this.applyConfiguration(factory); if (this.properties.getConfigurationProperties() != null) { factory.setConfigurationProperties(this.properties.getConfigurationProperties()); } if (!ObjectUtils.isEmpty(this.interceptors)) { factory.setPlugins(this.interceptors); } if (this.databaseIdProvider != null) { factory.setDatabaseIdProvider(this.databaseIdProvider); } if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) { factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage()); } if (this.properties.getTypeAliasesSuperType() != null) { factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType()); } if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) { factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage()); } if (!ObjectUtils.isEmpty(this.typeHandlers)) { factory.setTypeHandlers(this.typeHandlers); } if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) { factory.setMapperLocations(this.properties.resolveMapperLocations()); } Objects.requireNonNull(factory); this.getBeanThen(TransactionFactory.class, factory::setTransactionFactory); Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver(); if (!ObjectUtils.isEmpty(this.languageDrivers)) { factory.setScriptingLanguageDrivers(this.languageDrivers); } Optional var10000 = Optional.ofNullable(defaultLanguageDriver); Objects.requireNonNull(factory); var10000.ifPresent(factory::setDefaultScriptingLanguageDriver); this.applySqlSessionFactoryBeanCustomizers(factory); GlobalConfig globalConfig = this.properties.getGlobalConfig(); Objects.requireNonNull(globalConfig); this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler); this.getBeansThen(IKeyGenerator.class, (i) -> { globalConfig.getDbConfig().setKeyGenerators(i); }); Objects.requireNonNull(globalConfig); this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector); Objects.requireNonNull(globalConfig); this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator); factory.setGlobalConfig(globalConfig); return factory.getObject(); } @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ExecutorType executorType = this.properties.getExecutorType(); return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory); } }
这里主要关注配置类上面的注解,详细如下:
@Configuration( proxyBeanMethods = false ) // 当类路径下有SqlSessionFactory.class、SqlSessionFactoryBean.class时才生效 @ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class}) // 容器中只能有一个符合条件的DataSource // 因为容器中有3个数据源,且没有指定主数据源,这个条件不通过,就不会初始化这个配置类了 @ConditionalOnSingleCandidate(DataSource.class) @EnableConfigurationProperties({MybatisPlusProperties.class}) @AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class}) public class MybatisPlusAutoConfiguration implements InitializingBean { }
因为MybatisPlusAutoConfiguration不满足@ConditionalOnSingleCandidate(DataSource.class) 条件,所以自动配置不会生效,就不会执行sqlSessionFactory()、sqlSessionTemplate()两个方法。 容器中就不会有sqlSessionFactory、sqlSessionTemplate 两个bean对象,因此会报错。
所以在数据源配置类中的动态数据源配置上添加@Primary注解即可。
@Configuration @PropertySource("classpath:config/jdbc.properties") @MapperScan(basePackages = {"com.xinxing.learning.datasource.mapper"}) public class DynamicDataSourceConfig { @Bean @Primary public DataSource dynamicDataSource() { Map<Object, Object> targetDataSource = new HashMap<>(); targetDataSource.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource()); targetDataSource.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource()); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSource); dataSource.setDefaultTargetDataSource(masterDataSource()); return dataSource; } }#Java##Spring##计算机##程序员##编程#