文章内容
一、背景
在高并发的项目中,单数据库已无法承载大数据量的访问,因此需要使用多个数据库进行对数据的读写分离,此外就是在微服化的今天,我们在项目中可能采用各种不同存储,因此也需要连接不同的数据库,居于这样的背景,这里简单分享实现的思路以及实现方案。
二、如何实现
多数据源实现思路有两种。
1、通过配置多个SqlSessionFactory实现多数据源
一种是通过配置多个SqlSessionFactory实现多数据源;

2、通过Spring提供的AbstractRoutingDataSource实现动态切换数据源
另外一种是通过Spring提供的AbstractRoutingDataSource抽象了一个DynamicDataSource实现动态切换数据源;

三、实现方案
1、准备
采用Spring Boot2.7.8框架,数据库Mysql,ORM框架采用Mybatis,整个Maven依赖如下:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | < properties > < maven.compiler.source >8</ maven.compiler.source > < maven.compiler.target >8</ maven.compiler.target > < spring-boot.version >2.7.8</ spring-boot.version > < mysql-connector-java.version >5.1.46</ mysql-connector-java.version > < mybatis-spring-boot-starter.version >2.0.0</ mybatis-spring-boot-starter.version > < mybatis.version >3.5.1</ mybatis.version > </ properties > < dependencyManagement > < dependencies > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-dependencies</ artifactId > < version >${spring-boot.version}</ version > < type >pom</ type > < scope >import</ scope > </ dependency > < dependency > < groupId >mysql</ groupId > < artifactId >mysql-connector-java</ artifactId > < version >${mysql-connector-java.version}</ version > </ dependency > < dependency > < groupId >org.mybatis</ groupId > < artifactId >mybatis</ artifactId > < version >${mybatis.version}</ version > </ dependency > < dependency > < groupId >org.mybatis.spring.boot</ groupId > < artifactId >mybatis-spring-boot-starter</ artifactId > < version >${mybatis-spring-boot-starter.version}</ version > </ dependency > </ dependencies > </ dependencyManagement > |
2、指定数据源操作指定目录XML文件
该种方式需要操作的数据库的Mapper层和Dao层分别建立一个文件夹,分包放置,整体项目结构如下图:

3、Maven依赖
Maven依赖如下:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | < dependencies > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-web</ artifactId > </ dependency > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-test</ artifactId > </ dependency > < dependency > < groupId >com.zaxxer</ groupId > < artifactId >HikariCP</ artifactId > < version >4.0.3</ version > </ dependency > < dependency > < groupId >mysql</ groupId > < artifactId >mysql-connector-java</ artifactId > </ dependency > < dependency > < groupId >mysql</ groupId > < artifactId >mysql-connector-java</ artifactId > </ dependency > < dependency > < groupId >org.mybatis</ groupId > < artifactId >mybatis</ artifactId > </ dependency > < dependency > < groupId >org.mybatis.spring.boot</ groupId > < artifactId >mybatis-spring-boot-starter</ artifactId > </ dependency > < dependency > < groupId >junit</ groupId > < artifactId >junit</ artifactId > < scope >test</ scope > </ dependency > </ dependencies > |
4、Yaml文件
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | spring: datasource: user: jdbc-url: jdbc : mysql : //127 .0.0.1: 3306/study_user?useSSL= false &useUnicode= true &characterEncoding=UTF-8 username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource #hikari连接池配置 hikari: #pool name pool-name: user #最小空闲连接数 minimum-idle: 5 #最大连接池 maximum-pool-size: 20 #链接超时时间 3秒 connection-timeout: 3000 # 连接测试query connection-test-query: SELECT 1 soul: jdbc-url: jdbc : mysql : //127 .0.0.1: 3306/soul?useSSL= false &useUnicode= true &characterEncoding=UTF-8 username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource #hikari连接池配置 hikari: #pool name pool-name: soul #最小空闲连接数 minimum-idle: 5 #最大连接池 maximum-pool-size: 20 #链接超时时间 3秒 connection-timeout: 3000 # 连接测试query connection-test-query: SELECT 1 |
5、不同库的Mapper指定不同的SqlSessionFactory
针对不同的库分别放置对用不同的SqlSessionFactory
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | @Configuration @MapperScan (basePackages = "org.datasource.demo1.usermapper" , sqlSessionFactoryRef = "userSqlSessionFactory" ) public class UserDataSourceConfiguration { public static final String MAPPER_LOCATION = "classpath:usermapper/*.xml" ; @Primary @Bean ( "userDataSource" ) @ConfigurationProperties (prefix = "spring.datasource.user" ) public DataSource userDataSource() { return DataSourceBuilder.create().build(); } @Bean (name = "userTransactionManager" ) @Primary public PlatformTransactionManager userTransactionManager( @Qualifier ( "userDataSource" ) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Primary @Bean (name = "userSqlSessionFactory" ) public SqlSessionFactory userSqlSessionFactory( @Qualifier ( "userDataSource" ) DataSource dataSource) throws Exception { final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources(UserDataSourceConfiguration.MAPPER_LOCATION)); return sessionFactoryBean.getObject(); } } @Configuration @MapperScan (basePackages = "org.datasource.demo1.soulmapper" , sqlSessionFactoryRef = "soulSqlSessionFactory" ) public class SoulDataSourceConfiguration { public static final String MAPPER_LOCATION = "classpath:soulmapper/*.xml" ; @Bean ( "soulDataSource" ) @ConfigurationProperties (prefix = "spring.datasource.soul" ) public DataSource soulDataSource() { return DataSourceBuilder.create().build(); } @Bean (name = "soulTransactionManager" ) public PlatformTransactionManager soulTransactionManager( @Qualifier ( "soulDataSource" ) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean (name = "soulSqlSessionFactory" ) public SqlSessionFactory soulSqlSessionFactory( @Qualifier ( "soulDataSource" ) DataSource dataSource) throws Exception { final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources(SoulDataSourceConfiguration.MAPPER_LOCATION)); return sessionFactoryBean.getObject(); } } |
6、使用
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | @Service public class AppAuthService { @Autowired private AppAuthMapper appAuthMapper; @Transactional (rollbackFor = Exception. class ) public int getCount() { int a = appAuthMapper.listCount(); int b = 1 / 0 ; return a; } } @SpringBootTest @RunWith (SpringRunner. class ) public class TestDataSource { @Autowired private AppAuthService appAuthService; @Autowired private SysUserService sysUserService; @Test public void test_dataSource1(){ int b=sysUserService.getCount(); int a=appAuthService.getCount(); } } |
7、总结
此种方式使用起来分层明确,不存在任何冗余代码,不足地方就是每个库都需要对应一个配置类,该配置类中实现方式都基本类似,该种解决方案每个配置类中都存在事务管理器,因此不需要单独再去额外的关注。
四、AOP+自定义注解
关于采用Spring AOP方式实现原理就是把多个数据源存储在一个 Map中,当需要使用某个数据源时,从 Map中获取此数据源进行处理。

1、AbstractRoutingDataSource
在Spring中提供了AbstractRoutingDataSource来实现此功能,继承AbstractRoutingDataSource类并覆写其determineCurrentLookupKey()方法就可以完成数据源切换,该方法只需要返回数据源key即可,也就是存放数据源的Map的key。
接下来我们来看一下AbstractRoutingDataSource整体的继承结构,看它是如何做到的。

在整体的继承结构上我们会发现AbstractRoutingDataSource最终是继承于DataSource,因此当我们继承AbstractRoutingDataSource是我们自身也是一个数据源,对于数据源必然有连接数据库的动作,如下代码:
1 2 3 4 5 6 7 | public Connection getConnection() throws SQLException { return this .determineTargetDataSource().getConnection(); } public Connection getConnection(String username, String password) throws SQLException { return this .determineTargetDataSource().getConnection(username, password); } |
只是AbstractRoutingDataSource的getConnection()方法里实际是调用determineTargetDataSource()返回的数据源的getConnection()方法。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | protected DataSource determineTargetDataSource() { Assert.notNull( this .resolvedDataSources, "DataSource router not initialized" ); Object lookupKey = this .determineCurrentLookupKey(); DataSource 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 + "]" ); } else { return dataSource; } } |
该方法通过determineCurrentLookupKey()方法获取一个key,通过key从resolvedDataSources中获取数据源DataSource对象。determineCurrentLookupKey()是个抽象方法,需要继承AbstractRoutingDataSource的类实现;而resolvedDataSources是一个Map,里面应该保存当前所有可切换的数据源,接下来我们来聊聊实现,我们首先来看下目录,与分包的不同的是将所有的Mapper文件都放到一起,其他Maven依赖以及配置文件都保持一致。

2、DataSourceType
该枚举用来存放数据源的名称:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | public enum DataSourceType { USERDATASOURCE( "userDataSource" ), SOULDATASOURCE( "soulDataSource" ); private String name; DataSourceType(String name) { this .name=name; } public String getName() { return name; } public void setName(String name) { this .name = name; } } |
3、DynamicDataSourceConfiguration
通过读取配置文件中的数据源配置信息,创建数据连接,将多个数据源放入Map中,注入到容器中。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | @Configuration @MapperScan (basePackages = "org.datasource.demo2.mapper" ) public class DynamicDataSourceConfiguration { @Primary @Bean (name = "userDataSource" ) @ConfigurationProperties (prefix = "spring.datasource.user" ) public DataSource userDataSource() { return DataSourceBuilder.create().build(); } @Bean (name = "soulDataSource" ) @ConfigurationProperties (prefix = "spring.datasource.soul" ) public DataSource soulDataSource() { return DataSourceBuilder.create().build(); } @Bean (name = "dynamicDataSource" ) public DynamicDataSource DataSource( @Qualifier ( "userDataSource" ) DataSource userDataSource, @Qualifier ( "soulDataSource" ) DataSource soulDataSource) { //targetDataSource 集合是我们数据库和名字之间的映射 Map<Object, Object> targetDataSource = new HashMap<>(); targetDataSource.put(DataSourceType.USERDATASOURCE.getName(), userDataSource); targetDataSource.put(DataSourceType.SOULDATASOURCE.getName(), soulDataSource); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSource); //设置默认对象 dataSource.setDefaultTargetDataSource(userDataSource); return dataSource; } @Bean (name = "sqlSessionFactory" ) public SqlSessionFactory sqlSessionFactory( @Qualifier ( "dynamicDataSource" ) DataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setTransactionFactory( new MultiDataSourceTransactionFactory()); bean.setDataSource(dynamicDataSource); //设置我们的xml文件路径 bean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources( "classpath*:mapper/*.xml" )); return bean.getObject(); } } |
4、DataSourceContext
DataSourceContext使用ThreadLocal存放当前线程使用的数据源类型信息。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | public class DataSourceContext { private final static ThreadLocal<String> LOCAL_DATASOURCE = new ThreadLocal<String>(); public static void set(String name) { LOCAL_DATASOURCE.set(name); } public static String get() { return LOCAL_DATASOURCE.get(); } public static void remove() { LOCAL_DATASOURCE.remove(); } } |
5、DynamicDataSource
DynamicDataSource继承AbstractRoutingDataSource,重写determineCurrentLookupKey()方法,可以选择对应Key。
1 2 3 4 5 6 7 | public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContext.get(); } } |
6、CurrentDataSource
定义数据源的注解;
1 2 3 4 5 | @Retention (RetentionPolicy.RUNTIME) @Target (ElementType.METHOD) public @interface CurrentDataSource { DataSourceType value() default DataSourceType.USERDATASOURCE; } |
7、DataSourceAspect
定义切面切点,用来切换数据源。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | @Aspect @Order (- 1 ) @Component public class DataSourceAspect { @Pointcut ( "@annotation(org.datasource.demo2.constant.CurrentDataSource)" ) public void dsPointCut() { } @Around ( "dsPointCut()" ) public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); CurrentDataSource dataSource = method.getAnnotation(CurrentDataSource. class ); if (Objects.nonNull(dataSource)) { System.out.println( "切换数据源为" + dataSource.value().getName()); DataSourceContext.set(dataSource.value().getName()); } try { return point.proceed(); } finally { // 销毁数据源 在执行方法之后 System.out.println( "销毁数据源" + dataSource.value().getName()); DataSourceContext.remove(); } } } |
五、多数据源切换以后事务问题
Spring使用事务的方式有两种,一种是声明式事务,一种是编程式事务。
我们讨论的都是关于声明式事务,这种方式很方便,也是大家常用的,这里为什么讨论这个问题,当我们想将不同库的表放在同一个事务使用的时候,这个是时候我们会报错,如下图:

这部分也就是其他技术贴没讲解的部分,因此这里我们来补充一下这个话题,背过八股们的小伙伴都知道Spring事务是居于AOP实现,从这个角度很容易会理解到这个问题,当我们将两个Service方法放在同一个Transactional下的时候,这个代理对象就是当前类,因此导致数据源对象也是当前类下的DataSource,导致就出现表不存在问题,当Transactional分别放在不同Service的时候没有这种情况。
1 2 3 4 5 | @Transactional (rollbackFor = Exception. class ) public void update(){ sysUserMapper.updateSysUser( "111" ); appAuthService.update( "111" ); } |
有没有办法解决这个问题呢,当然是有的,这里我就不一步一步去探讨源码问题,我就直接直捣黄龙,把问题本质说一下,在Spring事务管理中有一个核心类DataSourceTransactionManager,该类是Spring事务核心的默认实现,AbstractPlatformTransactionManager是整体的Spring事务实现模板类,整体的继承结构如下图,

在方案一中,我们针对每个DataSourece都创建对应的DataSourceTransactionManager实现,也可以看出DataSourceTransactionManager就是管理我们整体的事务的,当我们配置了事物管理器以及拦截Service中的方法后,每次执行Service中方法前会开启一个事务,并且同时会缓存DataSource、SqlSessionFactory、Connection,因为DataSource、Conneciton都是从缓存中拿的,因此我们怎么切换数据源也没用,因此就出现表不存在的报错,具体源码可参考下面截图部分:


看到这里我们大致明白了为什么会报错,那么我们该如何做才能实现这种情况呢?其实我们要做的事就是动态的根据DataSourceType获取不同的Connection,不从缓存中获取Connection。
1、解决方案
我们来自定义一个MultiDataSourceTransaction实现Mybatis的事务接口,使用Map存储Connection相关连接,所有事务都采用手动提交,之后将MultiDataSourceTransaction交给SpringManagedTransactionFactory处理。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | public class MultiDataSourceTransaction implements Transaction { private final DataSource dataSource; private ConcurrentMap<String, Connection> concurrentMap; private boolean autoCommit; public MultiDataSourceTransaction(DataSource dataSource) { this .dataSource = dataSource; concurrentMap = new ConcurrentHashMap<>(); } @Override public Connection getConnection() throws SQLException { String databaseIdentification = DataSourceContext.get(); if (StringUtils.isEmpty(databaseIdentification)) { databaseIdentification = DataSourceType.USERDATASOURCE.getName(); } //获取数据源 if (! this .concurrentMap.containsKey(databaseIdentification)) { try { Connection conn = this .dataSource.getConnection(); autoCommit= false ; conn.setAutoCommit( false ); this .concurrentMap.put(databaseIdentification, conn); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException( "Could bot get JDBC otherConnection" , ex); } } return this .concurrentMap.get(databaseIdentification); } @Override public void commit() throws SQLException { for (Connection connection : concurrentMap.values()) { if (!autoCommit) { connection.commit(); } } } @Override public void rollback() throws SQLException { for (Connection connection : concurrentMap.values()) { connection.rollback(); } } @Override public void close() throws SQLException { for (Connection connection : concurrentMap.values()) { DataSourceUtils.releaseConnection(connection, this .dataSource); } } @Override public Integer getTimeout() throws SQLException { return null ; } } public class MultiDataSourceTransactionFactory extends SpringManagedTransactionFactory { @Override public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) { return new MultiDataSourceTransaction(dataSource); } } |
2、为什么可以这么做?
在Mybatis自动装配式会将配置文件装配为Configuration对象,也就是在方案一种SqlSessionFactory配置的过程,其中SqlSessionFactoryBean类实现了InitializingBean接口,初始化后执行afterPropertiesSet()方法,在afterPropertiesSet()方法中会执行 BuildSqlSessionFactory() 方法生成一个SqlSessionFactory对象。在BuildSqlSessionFactory中,会创建SpringManagedTransactionFactory对象,该对象就是MyBatis跟 Spring的桥梁。

在MapperScan自动扫描Mapper过程中,会通过ClassPathMapperScanner扫描器找到Mapper接口,封装成各自的BeanDefinition,然后循环遍历对Mapper的BeanDefinition修改beanClass为MapperFactoryBean。

由于MapperFactoryBean实现了FactoryBean,在Bean生命周期管理时会调用getObject方法,通过JDK动态代理生成代理对象MapperProxy,Mapper接口请求的时候,执行MapperProxy代理类的invoke方法,执行的过程中通过SqlSessionFactory创建的SqlSession去调用Executor执行器,进行数据库操作。下图是SqlSession创建的整个过程:

openSession方法是将Spring事务管理关联起来的核心代码,首先这里将通过 getTransactionFactoryFromEnvironment()方法获取TransactionFactory。这个操作会得到初始化时候注入的 SpringManagedTransactionFactory对象。然后将执行TransactionFactory#newTransaction() 方法,初始化 MyBatis的Transaction。

这里通过Configuration.newExecutor()创建一个Executor,Configuration指定在Executor默认为Simple,因此这里会创建一个SimpleExecutor,并初始化Transaction属性。接下来我们来看下SimpleExecutor执行执行update方法时候执行prepareStatement方法,在prepareStatement方法中执行了getConnection方法。


这里我们可以看到Connection获取过程,是通过Transaction获取的getConnection(),也就是通过之前注入的Transaction来获取Connection,这个Transaction就是SpringManagedTransaction,整体的时序图如下:


在整个调用链过程中,我们看到在DataSourceUtils有我们熟悉的TransactionSynchronizationManager,在上面Spring事务的时候我们也提到这个类,在开始Spring事务以后就会把Connetion绑定到当前线程,在DataSourceUtils获取到的Connection对象就是Srping开启事务时候创建的对象,这样就保证了Spring Transaction中的Connection跟MyBatis中执行SQL语句用的Connection为同一个 Connection,也就可以通过Spring事务管理机制进行事务管理了。

明白了整个流程,我们要做的事也就很简单,也就是每次切换DataSoure的同时获取最新的Connection,然后用一个Map对象来记录整个过程中的Connection,出现回滚这个Map对象里面Connection对象都回滚就可以了,然后将我们自定义的Transaction,委托给Spring在进行管理。
六、总结
采用AOP的方式是切换数据源已经非常好了,唯一不太好的地方就在于依然要手动去创建DataSource,每次增加都需要增加一个Bean,那有没有办法解决呢?
当然是有的,让我们来更上一层楼,解放双手。
1、ImportBeanDefinitionRegistrar
ImportBeanDefinitionRegistrar接口是Spring提供一个扩展点,主要用来注册BeanDefinition,常见的第三方框架在集成Spring的时候,都会通过该接口,实现扫描指定的类,然后注册到Spring容器中。比如 Mybatis中的Mapper接口,SpringCloud中的Feignlient接口,都是通过该接口实现的自定义注册逻辑。 我们要做的事情就是通过ImportBeanDefinitionRegistrar帮助我们动态的将DataSource扫描的到容器中去,不在采用增加Bean的方式。
整体代码如下:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | public class DynamicDataSourceBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware { /** * 默认dataSource */ private DataSource defaultDataSource; /** * 数据源map */ private Map<String, DataSource> dataSourcesMap = new HashMap<>(); @Override public void setEnvironment(Environment environment) { initConfig(environment); } private void initConfig(Environment env) { //读取配置文件获取更多数据源 String dsNames = env.getProperty( "spring.datasource.names" ); for (String dsName : dsNames.split( "," )) { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setPoolName(dsName); hikariConfig.setDriverClassName(env.getProperty( "spring.datasource." + dsName.trim() + ".driver-class-name" )); hikariConfig.setJdbcUrl(env.getProperty( "spring.datasource." + dsName.trim() + ".jdbc-url" )); hikariConfig.setUsername(env.getProperty( "spring.datasource." + dsName.trim() + ".username" )); hikariConfig.setPassword(env.getProperty( "spring.datasource." + dsName.trim() + ".password" )); hikariConfig.setConnectionTimeout(Long.parseLong(Objects.requireNonNull(env.getProperty( "spring.datasource." + dsName.trim() + ".hikari.connection-timeout" )))); hikariConfig.setMinimumIdle(Integer.parseInt(Objects.requireNonNull(env.getProperty( "spring.datasource." + dsName.trim() + ".hikari.minimum-idle" )))); hikariConfig.setMaximumPoolSize(Integer.parseInt(Objects.requireNonNull(env.getProperty( "spring.datasource." + dsName.trim() + ".hikari.maximum-pool-size" )))); hikariConfig.setConnectionInitSql( "SELECT 1" ); HikariDataSource dataSource = new HikariDataSource(hikariConfig); if (dataSourcesMap.size() == 0 ) { defaultDataSource = dataSource; } dataSourcesMap.put(dsName, dataSource); } } @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { Map<Object, Object> targetDataSources = new HashMap<Object, Object>(); //添加其他数据源 targetDataSources.putAll(dataSourcesMap); //创建DynamicDataSource GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(DynamicDataSource. class ); beanDefinition.setSynthetic( true ); MutablePropertyValues mpv = beanDefinition.getPropertyValues(); //defaultTargetDataSource 和 targetDataSources属性是 AbstractRoutingDataSource的两个属性Map mpv.addPropertyValue( "defaultTargetDataSource" , defaultDataSource); mpv.addPropertyValue( "targetDataSources" , targetDataSources); //注册 registry.registerBeanDefinition( "dataSource" , beanDefinition); } } |
2、@Import
@Import模式是向容器导入Bean是一种非常重要的方式,在注解驱动的Spring项目中,@Enablexxx的设计模式中有大量的使用,我们通过ImportBeanDefinitionRegistrar完成Bean的扫描,通过@Import导入到容器中,然后将EnableDynamicDataSource放入SpringBoot的启动项之上,到这里有没有感觉到茅塞顿开的感觉。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | @Target ({ElementType.TYPE}) @Retention (RetentionPolicy.RUNTIME) @Import ({DynamicDataSourceBeanDefinitionRegistrar. class }) public @interface EnableDynamicDataSource { } @SpringBootApplication @EnableAspectJAutoProxy @EnableDynamicDataSource public class DataSourceApplication { public static void main(String[] args) { SpringApplication.run(DataSourceApplication. class , args); } } |
3、DynamicDataSourceConfig
该类负责将Mapper扫描以及SpringFactory定义。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 | @Configuration @MapperScan (basePackages = "org.datasource.demo3.mapper" ) public class DynamicDataSourceConfig { @Autowired private DataSource dynamicDataSource; @Bean (name = "sqlSessionFactory" ) public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setTransactionFactory( new MultiDataSourceTransactionFactory()); bean.setDataSource(dynamicDataSource); //设置我们的xml文件路径 bean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources( "classpath*:mapper/*.xml" )); return bean.getObject(); } } |
4、yaml
关于yaml部分我们增加了names定义,方便识别出来配置了几个DataSource,剩下的部分与AOP保持一致。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | spring: datasource: names: user , soul user: jdbc-url: jdbc : mysql : //127 .0.0.1: 3306/study_user?useSSL= false &useUnicode= true &characterEncoding=UTF-8 username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource #hikari连接池配置 hikari: #最小空闲连接数 minimum-idle: 5 #最大连接池 maximum-pool-size: 20 #链接超时时间 3秒 connection-timeout: 3000 soul: jdbc-url: jdbc : mysql : //127 .0.0.1: 3306/soul?useSSL= false &useUnicode= true &characterEncoding=UTF-8 username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource #hikari连接池配置 hikari: #最小空闲连接数 minimum-idle: 5 #最大连接池 maximum-pool-size: 20 #链接超时时间 3秒 connection-timeout: 3000 |