在开发场景中,如果需要对接第三方系统的数据库,与自己的应用结合,这时就需要动态切换数据库进行对接
什么是多数据源 最常见的单一应用中最多涉及到一个数据库,既是一个数据源(DataSource
)。那么顾名思义,多数据源就是在一个单一应用中涉及到了两个及以上的数据库。
其实在配置数据源的时候就已经很明确这个定义了,如以下代码:
1 2 3 4 5 6 7 8 9 @Bean(name = "dataSource") public DataSource dataSource () { DruidDataSource druidDataSource = new DruidDataSource (); druidDataSource.setUrl(url); druidDataSource.setUsername(username); druidDataSource.setDriverClassName(driverClassName); druidDataSource.setPassword(password); return druidDataSource; }
url
、username
、password
这三个属性已经唯一确定了一个数据库了,DataSource
则是依赖这三个创建出来的。则多数据源即是配置多个DataSource
应用场景 相信大多数做过医疗系统的都会和HIS
打交道,为了简化护士以及医生的操作流程,必须要将必要的信息从HIS
系统对接过来,对接方式可以通过HIS
提供视图,比如医护视图,患者视图等,而此时其他系统只需要定时从HIS
视图中读取数据同步到自己数据库中即可。这是就涉及到了至少两个数据库了,一个是HIS
数据库,一个自己系统的数据库,在单一应用中必然需要用到多数据源切换
才能达到目的。
整合单一的数据源 本文使用阿里的数据库连接池druid
,添加依赖如下:
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.2.9</version > </dependency >
Druid连接池的starter
的自动配置类是DruidDataSourceAutoConfigure
,类上标注如下一行注解:
1 @EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@EnableConfigurationProperties
这个注解使得配置文件中的配置生效并且映射到指定类的属性
DruidStatProperties
中指定的前缀是spring.datasource.druid
,这个配置主要是用来设置连接池的一些参数。
DataSourceProperties
中指定的前缀是spring.datasource
,这个主要是用来设置数据库的url
、username
、password
等信息。
因此我们只需要在全局配置文件中指定数据库的一些配置
以及连接池的一些配置
信息即可,前缀分别是spring.datasource.druid
、spring.datasource
在全局配置文件application.yml
中配置数据库信息即可注入一个数据源到Spring Boot中。其实这仅仅是一种方式,下面介绍另外一种方式。
在自动配置类中DruidDataSourceAutoConfigure
中有如下一段代码:
1 2 3 4 5 6 @Bean(initMethod = "init") @ConditionalOnMissingBean public DataSource dataSource () { LOGGER.info("Init DruidDataSource" ); return new DruidDataSourceWrapper (); }
@ConditionalOnMissingBean
和@Bean
这两个注解的结合,意味着我们可以覆盖,只需要提前在IOC
中注入一个DataSource
类型的Bean
即可。
因此我们在自定义的配置类中定义如下配置即可:
1 2 3 4 5 6 7 8 9 10 11 @ConfigurationProperties(prefix = "spring.datasource") @Bean public DataSource dataSource () { return new DruidDataSource (); }
以上介绍了两种数据源的配置方式,第一种比较简单,第二种适合扩展,按需选择
整合Mybatis Spring Boot整合Mybatis其实很简单,简单的几步就搞定,首先添加依赖:
1 2 3 4 5 <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 2.0.0</version > </dependency >
第二步找到自动配置类MybatisAutoConfiguration
,有如下一行代码:
1 @EnableConfigurationProperties({MybatisProperties.class})
老套路了,全局配置文件中配置前缀为mybatis
的配置将会映射到该类中的属性
直接在全局配置文件配置各种属性是一种比较简单的方式,其实的任何组件的整合都有不少于两种的配置方式,下面来介绍下配置类如何配置。
MybatisAutoConfiguration
自动配置类中有如下一段代码:
1 2 3 @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory (DataSource dataSource) throws Exception {}
@ConditionalOnMissingBean
和@Bean
真是老搭档了,意外着我们又可以覆盖,只需要在IOC容器中注入SqlSessionFactory
(Mybatis六剑客之一生产者)
在自定义配置类中注入即可,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Bean("sqlSessionFactory1") public SqlSessionFactory sqlSessionFactory (DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean (); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver ().getResources("classpath*:/mapper/**/*.xml" )); org.apache.ibatis.session.Configuration configuration = new org .apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true ); configuration.setDefaultFetchSize(100 ); configuration.setDefaultStatementTimeout(30 ); sqlSessionFactoryBean.setConfiguration(configuration); return sqlSessionFactoryBean.getObject(); }
以上介绍了配置Mybatis的两种方式,其实在大多数场景中使用第一种已经够用了,至于为什么介绍第二种呢?当然是为了多数据源的整合而做准备了。
在MybatisAutoConfiguration
中有一行很重要的代码,如下:
1 @ConditionalOnSingleCandidate(DataSource.class)
@ConditionalOnSingleCandidate
这个注解的意思是当IOC容器中只有一个候选Bean的实例才会生效。言外之意就是当IOC容器中只有一个数据源DataSource,这个自动配置类才会生效
哦?照这样搞,多数据源是不是不能用Mybatis?
可能大家会有一个误解,认为多数据源就是多个的DataSource
并存的,当然这样说也不是不正确。
多数据源的情况下并不是多个数据源并存的,Spring提供了AbstractRoutingDataSource
这样一个抽象类,使得能够在多数据源的情况下任意切换,相当于一个动态路由
的作用,作者称之为动态数据源
。因此Mybatis只需要配置这个动态数据源即可。
什么是动态数据源 动态数据源简单的说就是能够自动切换的数据源,类似于一个动态路由的感觉,Spring提供了一个抽象类AbstractRoutingDataSource
,这个抽象类中有一个属性,如下:
1 private Map<Object, Object> targetDataSources;
targetDataSources是一个Map
结构,所有需要切换的数据源都存放在其中,根据指定的KEY进行切换。当然还有一个默认的数据源。
AbstractRoutingDataSource
这个抽象类中有一个抽象方法需要子类实现,如下:
1 protected abstract Object determineCurrentLookupKey () ;
determineCurrentLookupKey()
这个方法的返回值决定了需要切换的数据源的KEY
,就是根据这个KEY
从targetDataSources
取值(数据源)
数据源切换如何保证线程隔离? 数据源属于一个公共的资源,在多线程的情况下如何保证线程隔离呢?不能我这边切换了影响其他线程的执行。
“说到线程隔离,自然会想到ThreadLocal
了,将切换数据源的KEY
(用于从targetDataSources
中取值)存储在ThreadLocal
中,执行结束之后清除即可”
单独封装了一个DataSourceHolder
,内部使用ThreadLocal
隔离线程,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class DataSourceHolder { private static final ThreadLocal<String> dataSources = new InheritableThreadLocal (); public static void setDataSource (String datasource) { dataSources.set(datasource); } public static String getDataSource () { return dataSources.get(); } public static void clearDataSource () { dataSources.remove(); } }
如何构造一个动态数据源?
上文说过只需继承一个抽象类AbstractRoutingDataSource
,重写其中的一个方法determineCurrentLookupKey()
即可。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey () { return DataSourceHolder.getDataSource(); } public DynamicDataSource (DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) { super .setDefaultTargetDataSource(defaultTargetDataSource); super .setTargetDataSources(targetDataSources); super .afterPropertiesSet(); } }
上述代码很简单,分析如下:
一个多参的构造方法,指定了默认的数据源和目标数据源。
重写determineCurrentLookupKey()
方法,返回数据源对应的KEY
,这里是直接从ThreadLocal
中取值,就是上文封装的DataSourceHolder
定义一个注解 为了操作方便且低耦合,不能每次需要切换数据源的时候都要手动调一下接口吧,可以定义一个切换数据源的注解,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Target(value = ElementType.METHOD) @Retention(value = RetentionPolicy.RUNTIME) @Documented public @interface SwitchSource { String DEFAULT_NAME = "hisDataSource" ; String value () default DEFAULT_NAME; }
注解中只有一个value
属性,指定了需要切换数据源的KEY
有注解还不行,当然还要有切面,代码如下:
1 2 3 4 5 6 7 8 9 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 @Aspect @Order(1) @Component @Slf4j public class DataSourceAspect { @Pointcut("@annotation(SwitchSource)") public void pointcut () { } @Before(value = "pointcut()") public void beforeOpt (JoinPoint joinPoint) { Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); SwitchSource switchSource = method.getAnnotation(SwitchSource.class); log.info("[Switch DataSource]:" + switchSource.value()); DataSourceHolder.setDataSource(switchSource.value()); } @After(value = "pointcut()") public void afterOpt () { DataSourceHolder.clearDataSource(); log.info("[Switch Default DataSource]" ); } }
这个ASPECT
很容易理解,beforeOpt()
在方法之前执行,取值@SwitchSource
中value属性设置到ThreadLocal
中;afterOpt()
方法在方法执行之后执行,清除掉ThreadLocal
中的KEY
,保证了如果不切换数据源,则用默认的数据源。
如何与Mybatis整合? 单一数据源与Mybatis整合上文已经详细讲解了,数据源DataSource
作为参数构建了SqlSessionFactory
,同样的思想,只需要把这个数据源换成动态数据源即可。注入的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Primary @Bean("sqlSessionFactory") public SqlSessionFactory sqlSessionFactoryBean (DynamicDataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean (); sqlSessionFactoryBean.setDataSource(dynamicDataSource); org.apache.ibatis.session.Configuration configuration = new org .apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true ); configuration.setDefaultFetchSize(100 ); configuration.setDefaultStatementTimeout(30 ); sqlSessionFactoryBean.setConfiguration(configuration); return sqlSessionFactoryBean.getObject(); }
与Mybatis整合很简单,只需要把数据源替换成自定义的动态数据源DynamicDataSource
那么动态数据源如何注入到IOC容器中呢?看上文自定义的DynamicDataSource
构造方法,肯定需要两个数据源了,因此必须先注入两个或者多个数据源到IOC容器中,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @ConfigurationProperties(prefix = "spring.datasource") @Bean("dataSource") public DataSource dataSource () { return new DruidDataSource (); } @Bean(name = SwitchSource.DEFAULT_NAME) @ConfigurationProperties(prefix = "spring.datasource.his") public DataSource hisDataSource () { return new DruidDataSource (); }
以上构建的两个数据源,一个是默认的数据源
,一个是需要切换到的数据源(targetDataSources)
,这样就组成了动态数据源了。数据源的一些信息,比如url
,username
需要自己在全局配置文件中根据指定的前缀配置即可,代码不再贴出
动态数据源的注入代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Primary @Bean("sqlSessionFactory") public SqlSessionFactory sqlSessionFactoryBean (DynamicDataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean (); sqlSessionFactoryBean.setDataSource(dynamicDataSource); org.apache.ibatis.session.Configuration configuration = new org .apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true ); configuration.setDefaultFetchSize(100 ); configuration.setDefaultStatementTimeout(30 ); sqlSessionFactoryBean.setConfiguration(configuration); return sqlSessionFactoryBean.getObject(); }
这里还有一个问题:IOC中存在多个数据源了,那么事务管理器怎么办呢?它也懵逼了,到底选择哪个数据源呢?因此事务管理器肯定还是要重新配置的
事务管理器此时管理的数据源将是动态数据源DynamicDataSource
,配置如下:
1 2 3 4 5 6 7 8 @Primary @Bean(value = "transactionManager2") public PlatformTransactionManager annotationDrivenTransactionManager (DynamicDataSource dataSource) { return new DataSourceTransactionManager (dataSource); }
至此,Mybatis与多数据源的整合就完成了
源码地址 GitHub
参考@dd