Spring?Boot?優(yōu)雅整合多數(shù)據(jù)源
前言:
什么是多數(shù)據(jù)源?最常見的單一應(yīng)用中最多涉及到一個數(shù)據(jù)庫,即是一個數(shù)據(jù)源(Datasource)。那么顧名思義,多數(shù)據(jù)源就是在一個單一應(yīng)用中涉及到了兩個及以上的數(shù)據(jù)庫了。
其實在配置數(shù)據(jù)源的時候就已經(jīng)很明確這個定義了,如以下代碼:
@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這三個屬性已經(jīng)唯一確定了一個數(shù)據(jù)庫了,DataSource則是依賴這三個創(chuàng)建出來的。則多數(shù)據(jù)源即是配置多個DataSource(暫且這么理解)。
何時用到多數(shù)據(jù)源
正如前言介紹到的一個場景,相信大多數(shù)做過醫(yī)療系統(tǒng)的都會和HIS打交道,為了簡化護(hù)士以及醫(yī)生的操作流程,必須要將必要的信息從HIS系統(tǒng)對接過來,據(jù)我了解的大致有兩種方案如下:
HIS提供視圖,比如醫(yī)護(hù)視圖、患者視圖等,而此時其他系統(tǒng)只需要定時的從HIS視圖中讀取數(shù)據(jù)同步到自己數(shù)據(jù)庫中即可。
- HIS提供接口,無論是webService還是HTTP形式都是可行的,此時其他系統(tǒng)只需要按照要求調(diào)接口即可。
- 很明顯第一種方案涉及到了至少兩個數(shù)據(jù)庫了,一個是HIS數(shù)據(jù)庫,一個自己系統(tǒng)的數(shù)據(jù)庫,在單一應(yīng)用中必然需要用到多數(shù)據(jù)源的切換才能達(dá)到目的。
當(dāng)然多數(shù)據(jù)源的使用場景還是有很多的,以上只是簡單的一個場景。
整合單一的數(shù)據(jù)源
本文使用阿里的數(shù)據(jù)庫連接池druid,添加依賴如下:
<!--druid連接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.9</version> </dependency>
阿里的數(shù)據(jù)庫連接池非常強(qiáng)大,比如數(shù)據(jù)監(jiān)控、數(shù)據(jù)庫加密等等內(nèi)容,本文僅僅演示與Spring Boot整合的過程,一些其他的功能后續(xù)可以自己研究添加。
Druid連接池的starter的自動配置類是DruidDataSourceAutoConfigure,類上標(biāo)注如下一行注解:
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@EnableConfigurationProperties這個注解使得配置文件中的配置生效并且映射到指定類的屬性。
”DruidStatProperties中指定的前綴是spring.datasource.druid,這個配置主要是用來設(shè)置連接池的一些參數(shù)。
DataSourceProperties中指定的前綴是spring.datasource,這個主要是用來設(shè)置數(shù)據(jù)庫的url、username、password等信息。
因此我們只需要在全局配置文件中指定數(shù)據(jù)庫的一些配置以及連接池的一些配置信息即可,前綴分別是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 #連接池最大使用連接數(shù)量 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 #配置間隔多久才進(jìn)行一次檢測,檢測需要關(guān)閉的空閑連接,單位是毫秒 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功能,多少時間內(nèi)必須關(guān)閉連接 spring.datasource.druid.removeAbandoned=true #1800秒,也就是30分鐘 spring.datasource.druid.remove-abandoned-timeout=1800 #<!-- 1800秒,也就是30分鐘 --> spring.datasource.druid.log-abandoned=true spring.datasource.druid.filters=mergeStat
在全局配置文件application.properties文件中配置以上的信息即可注入一個數(shù)據(jù)源到Spring Boot中。其實這僅僅是一種方式,下面介紹另外一種方式。
”在自動配置類中DruidDataSourceAutoConfigure中有如下一段代碼:
@Bean(initMethod = "init") @ConditionalOnMissingBean public DataSource dataSource() { LOGGER.info("Init DruidDataSource"); return new DruidDataSourceWrapper(); }
@ConditionalOnMissingBean和@Bean這兩個注解的結(jié)合,意味著我們可以覆蓋,只需要提前在IOC中注入一個DataSource類型的Bean即可。
”因此我們在自定義的配置類中定義如下配置即可:
/** * @Bean:向IOC容器中注入一個Bean * @ConfigurationProperties:使得配置文件中以spring.datasource為前綴的屬性映射到Bean的屬性中 * @return */ @ConfigurationProperties(prefix = "spring.datasource") @Bean public DataSource dataSource(){ //做一些其他的自定義配置,比如密碼加密等...... return new DruidDataSource(); }
以上介紹了兩種數(shù)據(jù)源的配置方式,第一種比較簡單,第二種適合擴(kuò)展,按需選擇。
整合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六劍客之一生產(chǎn)者)。
在自定義配置類中注入即可,如下:
/** * 注入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(); // 自動將數(shù)據(jù)庫中的下劃線轉(zhuǎn)換為駝峰格式 configuration.setMapUnderscoreToCamelCase(true); configuration.setDefaultFetchSize(100); configuration.setDefaultStatementTimeout(30); sqlSessionFactoryBean.setConfiguration(configuration); return sqlSessionFactoryBean.getObject(); }
以上介紹了配置Mybatis的兩種方式,其實在大多數(shù)場景中使用第一種已經(jīng)夠用了,至于為什么介紹第二種呢?當(dāng)然是為了多數(shù)據(jù)源的整合而做準(zhǔn)備了。
”在MybatisAutoConfiguration中有一行很重要的代碼,如下:
@ConditionalOnSingleCandidate(DataSource.class)
@ConditionalOnSingleCandidate這個注解的意思是當(dāng)IOC容器中只有一個候選Bean的實例才會生效。
這行代碼標(biāo)注在Mybatis的自動配置類中有何含義呢?下面介紹,哈哈哈~
多數(shù)據(jù)源如何整合?
上文留下的問題:為什么的Mybatis自動配置上標(biāo)注如下一行代碼:
@ConditionalOnSingleCandidate(DataSource.class)
以上這行代碼的言外之意:當(dāng)IOC容器中只有一個數(shù)據(jù)源DataSource,這個自動配置類才會生效。
”哦?照這樣搞,多數(shù)據(jù)源是不能用Mybatis嗎?
可能大家會有一個誤解,認(rèn)為多數(shù)據(jù)源就是多個的DataSource并存的,當(dāng)然這樣說也不是不正確。
多數(shù)據(jù)源的情況下并不是多個數(shù)據(jù)源并存的,Spring提供了AbstractRoutingDataSource這樣一個抽象類,使得能夠在多數(shù)據(jù)源的情況下任意切換,相當(dāng)于一個動態(tài)路由的作用,作者稱之為動態(tài)數(shù)據(jù)源。因此Mybatis只需要配置這個動態(tài)數(shù)據(jù)源即可。
什么是動態(tài)數(shù)據(jù)源?
動態(tài)數(shù)據(jù)源簡單的說就是能夠自由切換的數(shù)據(jù)源,類似于一個動態(tài)路由的感覺,Spring 提供了一個抽象類AbstractRoutingDataSource,這個抽象類中喲一個屬性,如下:
private Map<Object, Object> targetDataSources;
targetDataSources是一個Map結(jié)構(gòu),所有需要切換的數(shù)據(jù)源都存放在其中,根據(jù)指定的KEY進(jìn)行切換。當(dāng)然還有一個默認(rèn)的數(shù)據(jù)源。
AbstractRoutingDataSource這個抽象類中有一個抽象方法需要子類實現(xiàn),如下:
protected abstract Object determineCurrentLookupKey();
determineCurrentLookupKey()這個方法的返回值決定了需要切換的數(shù)據(jù)源的KEY,就是根據(jù)這個KEY從targetDataSources取值(數(shù)據(jù)源)。
數(shù)據(jù)源切換如何保證線程隔離?
數(shù)據(jù)源屬于一個公共的資源,在多線程的情況下如何保證線程隔離呢?不能我這邊切換了影響其他線程的執(zhí)行。
說到線程隔離,自然會想到ThreadLocal了,將切換數(shù)據(jù)源的KEY(用于從targetDataSources中取值)存儲在ThreadLocal中,執(zhí)行結(jié)束之后清除即可。
”單獨(dú)封裝了一個DataSourceHolder,內(nèi)部使用ThreadLocal隔離線程,代碼如下:
/** * 使用ThreadLocal存儲切換數(shù)據(jù)源后的KEY */ public class DataSourceHolder { //線程 本地環(huán)境 private static final ThreadLocal<String> dataSources = new InheritableThreadLocal(); //設(shè)置數(shù)據(jù)源 public static void setDataSource(String datasource) { dataSources.set(datasource); } //獲取數(shù)據(jù)源 public static String getDataSource() { return dataSources.get(); } //清除數(shù)據(jù)源 public static void clearDataSource() { dataSources.remove(); } }
如何構(gòu)造一個動態(tài)數(shù)據(jù)源?
上文說過只需繼承一個抽象類AbstractRoutingDataSource,重寫其中的一個方法determineCurrentLookupKey()即可。
代碼如下:
/** * 動態(tài)數(shù)據(jù)源,繼承AbstractRoutingDataSource */ public class DynamicDataSource extends AbstractRoutingDataSource { /** * 返回需要使用的數(shù)據(jù)源的key,將會按照這個KEY從Map獲取對應(yīng)的數(shù)據(jù)源(切換) * @return */ @Override protected Object determineCurrentLookupKey() { //從ThreadLocal中取出KEY return DataSourceHolder.getDataSource(); } /** * 構(gòu)造方法填充Map,構(gòu)建多數(shù)據(jù)源 */ public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) { //默認(rèn)的數(shù)據(jù)源,可以作為主數(shù)據(jù)源 super.setDefaultTargetDataSource(defaultTargetDataSource); //目標(biāo)數(shù)據(jù)源 super.setTargetDataSources(targetDataSources); //執(zhí)行afterPropertiesSet方法,完成屬性的設(shè)置 super.afterPropertiesSet(); } }
上述代碼很簡單,分析如下:
- 一個多參的構(gòu)造方法,指定了默認(rèn)的數(shù)據(jù)源和目標(biāo)數(shù)據(jù)源。
- 重寫determineCurrentLookupKey()方法,返回數(shù)據(jù)源對應(yīng)的KEY,這里是直接從ThreadLocal中取值,就是上文封裝的DataSourceHolder。
定義一個注解
為了操作方便且低耦合,不能每次需要切換的數(shù)據(jù)源的時候都要手動調(diào)一下接口吧,可以定義一個切換數(shù)據(jù)源的注解,如下:
/** * 切換數(shù)據(jù)源的注解 */ @Target(value = ElementType.METHOD) @Retention(value = RetentionPolicy.RUNTIME) @Documented public @interface SwitchSource { /** * 默認(rèn)切換的數(shù)據(jù)源KEY */ String DEFAULT_NAME = "hisDataSource"; /** * 需要切換到數(shù)據(jù)的KEY */ String value() default DEFAULT_NAME; }
注解中只有一個value屬性,指定了需要切換數(shù)據(jù)源的KEY。
有注解還不行,當(dāng)然還要有切面,代碼如下:
@Aspect //優(yōu)先級要設(shè)置在事務(wù)切面執(zhí)行之前 @Order(1) @Component @Slf4j public class DataSourceAspect { @Pointcut("@annotation(SwitchSource)") public void pointcut() { } /** * 在方法執(zhí)行之前切換到指定的數(shù)據(jù)源 * @param joinPoint */ @Before(value = "pointcut()") public void beforeOpt(JoinPoint joinPoint) { /*因為是對注解進(jìn)行切面,所以這邊無需做過多判定,直接獲取注解的值,進(jìn)行環(huán)繞,將數(shù)據(jù)源設(shè)置成遠(yuǎn)方,然后結(jié)束后,清楚當(dāng)前線程數(shù)據(jù)源*/ Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); SwitchSource switchSource = method.getAnnotation(SwitchSource.class); log.info("[Switch DataSource]:" + switchSource.value()); DataSourceHolder.setDataSource(switchSource.value()); } /** * 方法執(zhí)行之后清除掉ThreadLocal中存儲的KEY,這樣動態(tài)數(shù)據(jù)源會使用默認(rèn)的數(shù)據(jù)源 */ @After(value = "pointcut()") public void afterOpt() { DataSourceHolder.clearDataSource(); log.info("[Switch Default DataSource]"); } }
這個ASPECT很容易理解,beforeOpt()在方法之前執(zhí)行,取值@SwitchSource中value屬性設(shè)置到ThreadLocal中;afterOpt()方法在方法執(zhí)行之后執(zhí)行,清除掉ThreadLocal中的KEY,保證了如果不切換數(shù)據(jù)源,則用默認(rèn)的數(shù)據(jù)源。
如何與Mybatis整合?
單一數(shù)據(jù)源與Mybatis整合上文已經(jīng)詳細(xì)講解了,數(shù)據(jù)源DataSource作為參數(shù)構(gòu)建了SqlSessionFactory,同樣的思想,只需要把這個數(shù)據(jù)源換成動態(tài)數(shù)據(jù)源即可。注入的代碼如下:
/** * 創(chuàng)建動態(tài)數(shù)據(jù)源的SqlSessionFactory,傳入的是動態(tài)數(shù)據(jù)源 * @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整合很簡單,只需要把數(shù)據(jù)源替換成自定義的動態(tài)數(shù)據(jù)源DynamicDataSource。
”那么動態(tài)數(shù)據(jù)源如何注入到IOC容器中呢?看上文自定義的DynamicDataSource構(gòu)造方法,肯定需要兩個數(shù)據(jù)源了,因此必須先注入兩個或者多個數(shù)據(jù)源到IOC容器中,如下:
/** * @Bean:向IOC容器中注入一個Bean * @ConfigurationProperties:使得配置文件中以spring.datasource為前綴的屬性映射到Bean的屬性中 */ @ConfigurationProperties(prefix = "spring.datasource") @Bean("dataSource") public DataSource dataSource(){ return new DruidDataSource(); } /** * 向IOC容器中注入另外一個數(shù)據(jù)源 * 全局配置文件中前綴是spring.datasource.his */ @Bean(name = SwitchSource.DEFAULT_NAME) @ConfigurationProperties(prefix = "spring.datasource.his") public DataSource hisDataSource() { return DataSourceBuilder.create().build(); }
以上構(gòu)建的兩個數(shù)據(jù)源,一個是默認(rèn)的數(shù)據(jù)源,一個是需要切換到的數(shù)據(jù)源(targetDataSources),這樣就組成了動態(tài)數(shù)據(jù)源了。數(shù)據(jù)源的一些信息,比如url,username需要自己在全局配置文件中根據(jù)指定的前綴配置即可,代碼不再貼出。
動態(tài)數(shù)據(jù)源的注入代碼如下:
/** * 創(chuàng)建動態(tài)數(shù)據(jù)源的SqlSessionFactory,傳入的是動態(tài)數(shù)據(jù)源 * @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中存在多個數(shù)據(jù)源了,那么事務(wù)管理器怎么辦呢?它也懵逼了,到底選擇哪個數(shù)據(jù)源呢?因此事務(wù)管理器肯定還是要重新配置的。
”事務(wù)管理器此時管理的數(shù)據(jù)源將是動態(tài)數(shù)據(jù)源DynamicDataSource,配置如下:
/** * 重寫事務(wù)管理器,管理動態(tài)數(shù)據(jù)源 */ @Primary @Bean(value = "transactionManager2") public PlatformTransactionManager annotationDrivenTransactionManager(DynamicDataSource dataSource) { return new DataSourceTransactionManager(dataSource); }
至此,Mybatis與多數(shù)據(jù)源的整合就完成了。
演示
使用也是很簡單,在需要切換數(shù)據(jù)源的方法上方標(biāo)注@SwitchSource切換到指定的數(shù)據(jù)源即可,如下:
//不開啟事務(wù) @Transactional(propagation = Propagation.NOT_SUPPORTED) //切換到HIS的數(shù)據(jù)源 @SwitchSource @Override public List<DeptInfo> list() { return hisDeptInfoMapper.listDept(); }
這樣只要執(zhí)行到這方法將會切換到HIS的數(shù)據(jù)源,方法執(zhí)行結(jié)束之后將會清除,執(zhí)行默認(rèn)的數(shù)據(jù)源。
總結(jié)
文章講了Spring Boot與單數(shù)據(jù)源、Mybatis、多數(shù)據(jù)源之間的整合,希望這篇文章能夠幫助讀者理解多數(shù)據(jù)源的整合,雖說用的不多,但是在有些領(lǐng)域仍然是比較重要的。
到此這篇關(guān)于Spring Boot 優(yōu)雅整合多數(shù)據(jù)源的文章就介紹到這了,更多相關(guān)Spring Boot 整合數(shù)據(jù)源內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springboot整合多數(shù)據(jù)源配置方式
- SpringBoot整合Mybatis Plus多數(shù)據(jù)源的實現(xiàn)示例
- SpringBoot+Mybatis plus實現(xiàn)多數(shù)據(jù)源整合的實踐
- 詳解Springboot之整合JDBCTemplate配置多數(shù)據(jù)源
- SpringBoot使用Atomikos技術(shù)整合多數(shù)據(jù)源的實現(xiàn)
- SpringBoot2整合JTA組件實現(xiàn)多數(shù)據(jù)源事務(wù)管理
- Springboot整合多數(shù)據(jù)源代碼示例詳解
- SpringBoot2整合Redis多數(shù)據(jù)源步驟詳解
相關(guān)文章
Java的this關(guān)鍵字的使用與方法的重載相關(guān)知識
這篇文章主要介紹了Java的this關(guān)鍵字的使用與方法的重載相關(guān)知識,是Java入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2015-09-09深度解析Java中的國際化底層類ResourceBundle
做項目應(yīng)該都會實現(xiàn)國際化,那么大家知道Java底層是如何實現(xiàn)國際化的嗎?這篇文章就來和大家深度解析一下Java中的國際化底層類ResourceBundle,希望對大家有所幫助2023-03-03Java之Swagger配置掃描接口以及開關(guān)案例講解
這篇文章主要介紹了Java之Swagger配置掃描接口以及開關(guān)案例講解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08Java使用httpRequest+Jsoup爬取紅藍(lán)球號碼
本文將結(jié)合實例代碼,介紹Java使用httpRequest+Jsoup爬取紅藍(lán)球號碼,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-07-07Java后端限制頻繁請求和重復(fù)提交的實現(xiàn)
很多用戶會請求過于頻繁或者是多次重復(fù)提交數(shù)據(jù),本文主要介紹了Java后端限制頻繁請求和重復(fù)提交的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-04-04