Spring Boot 整合多數據源,這才叫優雅~

語言: CN / TW / HK

什麼是多數據源?最常見的單一應用中最多涉及到一個數據庫,即是一個數據源(Datasource)。那麼顧名思義,多數據源就是在一個單一應用中涉及到了兩個及以上的數據庫了。

其實在配置數據源的時候就已經很明確這個定義了,如以下代碼:

@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提供接口,無論是webService還是HTTP形式都是可行的,此時其他系統只需要按照要求調接口即可。
  • 很明顯第一種方案涉及到了至少兩個數據庫了,一個是HIS數據庫,一個自己系統的數據庫,在單一應用中必然需要用到多數據源的切換才能達到目的。

當然多數據源的使用場景還是有很多的,以上只是簡單的一個場景。

整合單一的數據源

本文使用阿里的數據庫連接池druid,添加依賴如下:

```

com.alibaba druid-spring-boot-starter 1.1.9 ```

阿里的數據庫連接池非常強大,比如數據監控、數據庫加密等等內容,本文僅僅演示與Spring Boot整合的過程,一些其他的功能後續可以自己研究添加。

Druid連接池的starter的自動配置類是DruidDataSourceAutoConfigure,類上標註如下一行註解:

@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})

@EnableConfigurationProperties這個註解使得配置文件中的配置生效並且映射到指定類的屬性。

”DruidStatProperties中指定的前綴是spring.datasource.druid,這個配置主要是用來設置連接池的一些參數。

DataSourceProperties中指定的前綴是spring.datasource,這個主要是用來設置數據庫的url、username、password等信息。

因此我們只需要在全局配置文件中指定數據庫的一些配置以及連接池的一些配置信息即可,前綴分別是spring.datasource.druid、spring.datasource,以下是個人隨便配置的(application.properties):

``` spring.datasource.url=jdbc:mysql://120.26.101.xxx:3306/xxx?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=xxxx spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.jdbc.Driver

初始化連接大小

spring.datasource.druid.initial-size=0

連接池最大使用連接數量

spring.datasource.druid.max-active=20

連接池最小空閒

spring.datasource.druid.min-idle=0

獲取連接最大等待時間

spring.datasource.druid.max-wait=6000 spring.datasource.druid.validation-query=SELECT 1

spring.datasource.druid.validation-query-timeout=6000

spring.datasource.druid.test-on-borrow=false spring.datasource.druid.test-on-return=false spring.datasource.druid.test-while-idle=true

配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒

spring.datasource.druid.time-between-eviction-runs-millis=60000

置一個連接在池中最小生存的時間,單位是毫秒

spring.datasource.druid.min-evictable-idle-time-millis=25200000

spring.datasource.druid.max-evictable-idle-time-millis=

打開removeAbandoned功能,多少時間內必須關閉連接

spring.datasource.druid.removeAbandoned=true

1800秒,也就是30分鐘

spring.datasource.druid.remove-abandoned-timeout=1800

spring.datasource.druid.log-abandoned=true spring.datasource.druid.filters=mergeStat ```

在全局配置文件application.properties文件中配置以上的信息即可注入一個數據源到Spring Boot中。其實這僅僅是一種方式,下面介紹另外一種方式。

”在自動配置類中DruidDataSourceAutoConfigure中有如下一段代碼:

@Bean(initMethod = "init") @ConditionalOnMissingBean public DataSource dataSource() { LOGGER.info("Init DruidDataSource"); return new DruidDataSourceWrapper(); }

@ConditionalOnMissingBean和@Bean這兩個註解的結合,意味着我們可以覆蓋,只需要提前在IOC中注入一個DataSource類型的Bean即可。

”因此我們在自定義的配置類中定義如下配置即可:

/** * @Bean:向IOC容器中注入一個Bean * @ConfigurationProperties:使得配置文件中以spring.datasource為前綴的屬性映射到Bean的屬性中 * @return */ @ConfigurationProperties(prefix = "spring.datasource") @Bean public DataSource dataSource(){ //做一些其他的自定義配置,比如密碼加密等...... return new DruidDataSource(); }

以上介紹了兩種數據源的配置方式,第一種比較簡單,第二種適合擴展,按需選擇。

整合Mybatis

Spring Boot 整合Mybatis其實很簡單,簡單的幾步就搞定,首先添加依賴:

<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency>

第二步找到自動配置類MybatisAutoConfiguration,有如下一行代碼:

@EnableConfigurationProperties(MybatisProperties.class)

老套路了,全局配置文件中配置前綴為mybatis的配置將會映射到該類中的屬性。

”可配置的東西很多,比如XML文件的位置、類型處理器等等,如下簡單的配置:

mybatis.type-handlers-package=com.demo.typehandler mybatis.configuration.map-underscore-to-camel-case=true

如果需要通過包掃描的方式注入Mapper,則需要在配置類上加入一個註解:@MapperScan,其中的value屬性指定需要掃描的包。

直接在全局配置文件配置各種屬性是一種比較簡單的方式,其實的任何組件的整合都有不少於兩種的配置方式,下面來介紹下配置類如何配置。

”MybatisAutoConfiguration自動配置類有如下一斷代碼:

@Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {}

@ConditionalOnMissingBean和@Bean真是老搭檔了,意味着我們又可以覆蓋,只需要在IOC容器中注入SqlSessionFactory(Mybatis六劍客之一生產者)。

在自定義配置類中注入即可,如下:

/** * 注入SqlSessionFactory */ @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中有一行很重要的代碼,如下:

@ConditionalOnSingleCandidate(DataSource.class)

@ConditionalOnSingleCandidate這個註解的意思是當IOC容器中只有一個候選Bean的實例才會生效。

這行代碼標註在Mybatis的自動配置類中有何含義呢?下面介紹,哈哈哈~

多數據源如何整合?

上文留下的問題:為什麼的Mybatis自動配置上標註如下一行代碼:

@ConditionalOnSingleCandidate(DataSource.class)

以上這行代碼的言外之意:當IOC容器中只有一個數據源DataSource,這個自動配置類才會生效。

”哦?照這樣搞,多數據源是不能用Mybatis嗎?

可能大家會有一個誤解,認為多數據源就是多個的DataSource並存的,當然這樣説也不是不正確。

多數據源的情況下並不是多個數據源並存的,Spring提供了AbstractRoutingDataSource這樣一個抽象類,使得能夠在多數據源的情況下任意切換,相當於一個動態路由的作用,作者稱之為動態數據源。因此Mybatis只需要配置這個動態數據源即可。

什麼是動態數據源?

動態數據源簡單的説就是能夠自由切換的數據源,類似於一個動態路由的感覺,Spring 提供了一個抽象類AbstractRoutingDataSource,這個抽象類中喲一個屬性,如下:

private Map<Object, Object> targetDataSources;

targetDataSources是一個Map結構,所有需要切換的數據源都存放在其中,根據指定的KEY進行切換。當然還有一個默認的數據源。

AbstractRoutingDataSource這個抽象類中有一個抽象方法需要子類實現,如下:

protected abstract Object determineCurrentLookupKey();

determineCurrentLookupKey()這個方法的返回值決定了需要切換的數據源的KEY,就是根據這個KEY從targetDataSources取值(數據源)。

數據源切換如何保證線程隔離?

數據源屬於一個公共的資源,在多線程的情況下如何保證線程隔離呢?不能我這邊切換了影響其他線程的執行。

説到線程隔離,自然會想到ThreadLocal了,將切換數據源的KEY(用於從targetDataSources中取值)存儲在ThreadLocal中,執行結束之後清除即可。

”單獨封裝了一個DataSourceHolder,內部使用ThreadLocal隔離線程,代碼如下:

``` /* * 使用ThreadLocal存儲切換數據源後的KEY / 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()即可。代碼如下:

``` /* * 動態數據源,繼承AbstractRoutingDataSource / public class DynamicDataSource extends AbstractRoutingDataSource {

/**
 * 返回需要使用的數據源的key,將會按照這個KEY從Map獲取對應的數據源(切換)
 * @return
 */
@Override
protected Object determineCurrentLookupKey() {
    //從ThreadLocal中取出KEY
    return DataSourceHolder.getDataSource();
}

/**
 * 構造方法填充Map,構建多數據源
 */
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
    //默認的數據源,可以作為主數據源
    super.setDefaultTargetDataSource(defaultTargetDataSource);
    //目標數據源
    super.setTargetDataSources(targetDataSources);
    //執行afterPropertiesSet方法,完成屬性的設置
    super.afterPropertiesSet();
}

} ```

上述代碼很簡單,分析如下:

  • 一個多參的構造方法,指定了默認的數據源和目標數據源。
  • 重寫determineCurrentLookupKey()方法,返回數據源對應的KEY,這裏是直接從ThreadLocal中取值,就是上文封裝的DataSourceHolder。

定義一個註解

為了操作方便且低耦合,不能每次需要切換的數據源的時候都要手動調一下接口吧,可以定義一個切換數據源的註解,如下:

``` /* * 切換數據源的註解 / @Target(value = ElementType.METHOD) @Retention(value = RetentionPolicy.RUNTIME) @Documented public @interface SwitchSource {

/**
 * 默認切換的數據源KEY
 */
String DEFAULT_NAME = "hisDataSource";

/**
 * 需要切換到數據的KEY
 */
String value() default DEFAULT_NAME;

} ```

註解中只有一個value屬性,指定了需要切換數據源的KEY。

有註解還不行,當然還要有切面,代碼如下:

``` @Aspect //優先級要設置在事務切面執行之前 @Order(1) @Component @Slf4j public class DataSourceAspect {

@Pointcut("@annotation(SwitchSource)")
public void pointcut() {
}

/**
 * 在方法執行之前切換到指定的數據源
 * @param joinPoint
 */
@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());
}

/**
 * 方法執行之後清除掉ThreadLocal中存儲的KEY,這樣動態數據源會使用默認的數據源
 */
@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,同樣的思想,只需要把這個數據源換成動態數據源即可。注入的代碼如下:

/** * 創建動態數據源的SqlSessionFactory,傳入的是動態數據源 * @Primary這個註解很重要,如果項目中存在多個SqlSessionFactory,這個註解一定要加上 */ @Primary @Bean("sqlSessionFactory2") public SqlSessionFactory sqlSessionFactoryBean(DynamicDataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dynamicDataSource); 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整合很簡單,只需要把數據源替換成自定義的動態數據源DynamicDataSource。

”那麼動態數據源如何注入到IOC容器中呢?看上文自定義的DynamicDataSource構造方法,肯定需要兩個數據源了,因此必須先注入兩個或者多個數據源到IOC容器中,如下:

``` /* * @Bean:向IOC容器中注入一個Bean * @ConfigurationProperties:使得配置文件中以spring.datasource為前綴的屬性映射到Bean的屬性中 / @ConfigurationProperties(prefix = "spring.datasource") @Bean("dataSource") public DataSource dataSource(){ return new DruidDataSource(); }

/**
 * 向IOC容器中注入另外一個數據源
 * 全局配置文件中前綴是spring.datasource.his
 */
@Bean(name = SwitchSource.DEFAULT_NAME)
@ConfigurationProperties(prefix = "spring.datasource.his")
public DataSource hisDataSource() {
    return DataSourceBuilder.create().build();
}

```

以上構建的兩個數據源,一個是默認的數據源,一個是需要切換到的數據源(targetDataSources),這樣就組成了動態數據源了。數據源的一些信息,比如url,username需要自己在全局配置文件中根據指定的前綴配置即可,代碼不再貼出。

”動態數據源的注入代碼如下:

/** * 創建動態數據源的SqlSessionFactory,傳入的是動態數據源 * @Primary這個註解很重要,如果項目中存在多個SqlSessionFactory,這個註解一定要加上 */ @Primary @Bean("sqlSessionFactory2") 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,配置如下:

/** * 重寫事務管理器,管理動態數據源 */ @Primary @Bean(value = "transactionManager2") public PlatformTransactionManager annotationDrivenTransactionManager(DynamicDataSource dataSource) { return new DataSourceTransactionManager(dataSource); }

至此,Mybatis與多數據源的整合就完成了。

演示

使用也是很簡單,在需要切換數據源的方法上方標註@SwitchSource切換到指定的數據源即可,如下:

//不開啟事務 @Transactional(propagation = Propagation.NOT_SUPPORTED) //切換到HIS的數據源 @SwitchSource @Override public List<DeptInfo> list() { return hisDeptInfoMapper.listDept(); }

這樣只要執行到這方法將會切換到HIS的數據源,方法執行結束之後將會清除,執行默認的數據源。

總結

本篇文章講了Spring Boot與單數據源、Mybatis、多數據源之間的整合,希望這篇文章能夠幫助讀者理解多數據源的整合,雖説用的不多,但是在有些領域仍然是比較重要的。