簡要原理: 1)DataSourceEnum列出所有的數據源的key key 2)DataSourceHolder是一個線程安全的DataSourceEnum容器,並提供了向其中設置和獲取DataSourceEnum的方法 3)DynamicDataSource繼承AbstractRoutingDa ...
簡要原理:
1)DataSourceEnum列出所有的數據源的key---key
2)DataSourceHolder是一個線程安全的DataSourceEnum容器,並提供了向其中設置和獲取DataSourceEnum的方法
3)DynamicDataSource繼承AbstractRoutingDataSource並重寫其中的方法determineCurrentLookupKey(),在該方法中使用DataSourceHolder獲取當前線程的DataSourceEnum
4)MyBatisConfig中生成2個數據源DataSource的bean---value
5)MyBatisConfig中將1)和4)組成的key-value對寫入到DynamicDataSource動態數據源的targetDataSources屬性(當然,同時也會設置2個數據源其中的一個為DynamicDataSource的defaultTargetDataSource屬性中)
6)將DynamicDataSource作為primary數據源註入到SqlSessionFactory的dataSource屬性中去,並且該dataSource作為transactionManager的入參來構造DataSourceTransactionManager
7)使用spring aop根據不同的包設置不同的數據源(DataSourceExchange),先使用DataSourceHolder設置將要使用的數據源key
註意:在mapper層進行操作的時候,會先調用determineCurrentLookupKey()方法獲取一個數據源(獲取數據源:先根據設置去targetDataSources中去找,若沒有,則選擇defaultTargetDataSource),之後在進行資料庫操作。
DataSourceEnum
package com.theeternity.common.dataSource;
/**
* @program: boxApi
* @description: 多數據源枚舉類
* @author: tonyzhang
* @create: 2018-12-18 11:14
*/
public enum DataSourceEnum {
/**
* @Description: DS1數據源1, DS2數據源2
* @Param:
* @return:
* @Author: tonyzhang
* @Date: 2018-12-18 11:20
*/
DS1("ds1"), DS2("ds2");
private String key;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
DataSourceEnum(String key) {
this.key = key;
}
}
DataSourceHolder
作用:構建一個DatabaseType容器,並提供了向其中設置和獲取DataSourceEnmu的方法
package com.theeternity.common.dataSource;
/**
* @program: boxApi
* @description: DynamicDataSourceHolder用於持有當前線程中使用的數據源標識
* @author: tonyzhang
* @create: 2018-12-18 11:16
*/
public class DataSourceHolder {
private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();
public static void setDataSources(String dataSource) {
dataSources.set(dataSource);
}
public static String getDataSources() {
return dataSources.get();
}
}
DynamicDataSource
作用:使用DatabaseContextHolder獲取當前線程的DataSourceEnmu
package com.theeternity.common.dataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @program: boxApi
* @description: DynamicDataSource的類,繼承AbstractRoutingDataSource並重寫determineCurrentLookupKey方法
* @author: tonyzhang
* @create: 2018-12-18 11:17
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceHolder.getDataSources();
}
}
MyBatisConfig
作用:
通過讀取application-test.yml文件生成兩個數據源(writeDS、readDS)
使用以上生成的兩個數據源構造動態數據源dataSource
@Primary:指定在同一個介面有多個實現類可以註入的時候,預設選擇哪一個,而不是讓@Autowire註解報錯(一般用於多數據源的情況下)
@Qualifier:指定名稱的註入,當一個介面有多個實現類的時候使用(在本例中,有兩個DataSource類型的實例,需要指定名稱註入)
@Bean:生成的bean實例的名稱是方法名(例如上邊的@Qualifier註解中使用的名稱是前邊兩個數據源的方法名,而這兩個數據源也是使用@Bean註解進行註入的)
通過動態數據源構造SqlSessionFactory和事務管理器(如果不需要事務,後者可以去掉)
package com.theeternity.beans.mybatisConfig;
import com.theeternity.common.dataSource.DataSourceEnum;
import com.theeternity.common.dataSource.DynamicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @program: ApiBoot
* @description: 動態數據源配置
* @author: TheEternity Zhang
* @create: 2019-02-18 11:04
*/
@Configuration
public class MyBatisConfig {
@Value("${spring.datasource.type}")
private Class<? extends DataSource> dataSourceType;
@Value("${mybatis.type-aliases-package}")
private String basicPackage;
@Value("${mybatis-plus.mapper-locations}")
private String mapperLocation;
@Bean(name="writeDS")
@ConfigurationProperties(prefix = "primary.datasource.druid")
public DataSource writeDataSource() {
return DataSourceBuilder.create().type(dataSourceType).build();
}
/**
* 有多少個從庫就要配置多少個
* @return
*/
@Bean(name = "readDS")
@ConfigurationProperties(prefix = "back.datasource.druid")
public DataSource readDataSourceOne(){
return DataSourceBuilder.create().type(dataSourceType).build();
}
/**
* @Primary 該註解表示在同一個介面有多個實現類可以註入的時候,預設選擇哪一個,而不是讓@autowire註解報錯
* @Qualifier 根據名稱進行註入,通常是在具有相同的多個類型的實例的一個註入(例如有多個DataSource類型的實例)
*/
@Bean
@Primary
public DynamicDataSource dataSource(@Qualifier("writeDS") DataSource writeDS,
@Qualifier("readDS") DataSource readDS) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceEnum.DS1.getKey(), writeDS);
targetDataSources.put(DataSourceEnum.DS2.getKey(), readDS);
DynamicDataSource dataSource =new DynamicDataSource();
// 該方法是AbstractRoutingDataSource的方法
dataSource.setTargetDataSources(targetDataSources);
// 預設的datasource設置為writeDS
dataSource.setDefaultTargetDataSource(writeDS);
return dataSource;
}
/**
* 根據數據源創建SqlSessionFactory
*/
@Bean
public SqlSessionFactory sqlSessionFactory(DynamicDataSource dataSource) throws Exception {
SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
// 指定數據源(這個必須有,否則報錯)
fb.setDataSource(dataSource);
// 下邊兩句僅僅用於*.xml文件,如果整個持久層操作不需要使用到xml文件的話(只用註解就可以搞定),則不加
// 指定基包
fb.setTypeAliasesPackage(basicPackage);
fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocation));
return fb.getObject();
}
/**
* 配置事務管理器
*/
@Bean
public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
return new DataSourceTransactionManager(dataSource);
}
}
DataSourceExchange
package com.theeternity.common.aop;
import com.theeternity.common.dataSource.DataSourceEnum;
import com.theeternity.common.dataSource.DataSourceHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* @program: boxApi
* @description: 數據源自動切換AOP
* @author: tonyzhang
* @create: 2018-12-18 11:20
*/
@Aspect
@Component
public class DataSourceExchange {
private Logger logger= LoggerFactory.getLogger(DataSourceExchange.class);
@Pointcut("execution(* com.theeternity.core.*.service..*(..))")
public void pointcut(){}
/**
* @Description: 在service方法開始之前切換數據源
* @Param: [joinPoint]
* @return: void
* @Author: tonyzhang
* @Date: 2018-12-18 11:28
*/
@Before(value="pointcut()")
public void before(JoinPoint joinPoint){
//獲取目標對象的類類型
Class<?> aClass = joinPoint.getTarget().getClass();
String c = aClass.getName();
System.out.println("作用包名:"+c);
String[] ss = c.split("\\.");
//獲取包名用於區分不同數據源
String packageName = ss[3];
System.out.println("包名:"+packageName);
if ("AutoGenerator".equals(packageName)) {
DataSourceHolder.setDataSources(DataSourceEnum.DS1.getKey());
logger.info("數據源:"+DataSourceEnum.DS1.getKey());
} else {
DataSourceHolder.setDataSources(DataSourceEnum.DS2.getKey());
logger.info("數據源:"+DataSourceEnum.DS2.getKey());
}
}
/**
* @Description: 執行完畢之後將數據源清空
* @Param: [joinPoint]
* @return: void
* @Author: tonyzhang
* @Date: 2018-12-18 11:27
*/
@After(value="pointcut()")
public void after(JoinPoint joinPoint){
DataSourceHolder.setDataSources(null);
}
}
屏蔽springboot自帶的自動註冊數據源
很多朋友反映遇到數據源迴圈依賴的問題,可以試一下將MyBatisConfig中的相關代碼換成這樣試試
首先要將spring boot自帶的DataSourceAutoConfiguration禁掉,因為它會讀取application.properties文件的spring.datasource.*屬性並自動配置單數據源。在@SpringBootApplication註解中添加exclude屬性即可:
package com.theeternity.core;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(scanBasePackages = "com.theeternity",exclude = {DataSourceAutoConfiguration.class})
/**
* 全局配置,掃描指定包下的dao介面,不用每個dao介面上都寫@Mapper註解了
*/
@MapperScan("com.theeternity.core.*.dao")
public class CoreApplication {
public static void main(String[] args) {
SpringApplication.run(CoreApplication.class, args);
}
}
如果不屏蔽DataSourceAutoConfiguration可以使用如下測試一下(待測試)
將MyBatisConfig中SqlSessionFactory的構建方法改為下麵的
@Bean
2 public SqlSessionFactory sqlSessionFactory(@Qualifier("writeDS") DataSource writeDS,
3 @Qualifier("readDS") DataSource readDS) throws Exception{
4 SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
5 fb.setDataSource(this.dataSource(writeDS, readDS));
6 fb.setTypeAliasesPackage(basicPackage);
fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocation));
8 return fb.getObject();
9 }
主配置文件
#配置使用的文件
spring:
profiles:
active: test,redis
devtools:
restart:
enabled: true
additional-paths: src/main/java
#配置tomcat埠及路徑
server:
port: 8088
servlet:
context-path: /core
#mybatis配置
mybatis:
mapper-locations: classpath*:/mybatis-mapper/*.xml
type-aliases-package: com.theeternity.core.*.entity
configuration:
map-underscore-to-camel-case: true #駝峰命名
#mybatis plus配置
mybatis-plus:
mapper-locations: classpath*:/mybatis-plus-mapper/*.xml
# MyBaits 別名包掃描路徑,通過該屬性可以給包中的類註冊別名,註冊後在 Mapper 對應的 XML 文件中可以直接使用類名,而不用使用全限定的類名
type-aliases-package: com.theeternity.core.*.entity
# 資料庫表與實體類的駝峰命名自動轉換
configuration:
map-underscore-to-camel-case: true
配置文件application-test.yml
spring:
datasource:
#使用druid連接池
type: com.alibaba.druid.pool.DruidDataSource
# 自定義的主數據源配置信息
primary:
datasource:
#druid相關配置
druid:
#監控統計攔截的filters
filters: stat
driverClassName: com.mysql.cj.jdbc.Driver
#配置基本屬性
url: jdbc:mysql://localhost:3306/wechatMVC?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
username: ***
password: ***
#配置初始化大小/最小/最大
initialSize: 1
minIdle: 1
maxActive: 20
#獲取連接等待超時時間
maxWait: 60000
#間隔多久進行一次檢測,檢測需要關閉的空閑連接
timeBetweenEvictionRunsMillis: 60000
#一個連接在池中最小生存的時間
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
#打開PSCache,並指定每個連接上PSCache的大小。oracle設為true,mysql設為false。分庫分表較多推薦設置為false
poolPreparedStatements: false
maxPoolPreparedStatementPerConnectionSize: 20
# 自定義的從數據源配置信息
back:
datasource:
#druid相關配置
druid:
#監控統計攔截的filters
filters: stat
driverClassName: com.mysql.cj.jdbc.Driver
#配置基本屬性
url: jdbc:mysql://localhost:3306/mycrm?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
username: ***
password: ***
#配置初始化大小/最小/最大
initialSize: 1
minIdle: 1
maxActive: 20
#獲取連接等待超時時間
maxWait: 60000
#間隔多久進行一次檢測,檢測需要關閉的空閑連接
timeBetweenEvictionRunsMillis: 60000
#一個連接在池中最小生存的時間
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
#打開PSCache,並指定每個連接上PSCache的大小。oracle設為true,mysql設為false。分庫分表較多推薦設置為false
poolPreparedStatements: false
maxPoolPreparedStatementPerConnectionSize: 20
參考文檔:
https://www.cnblogs.com/java-zhao/p/5413845.html (主參考流程)
http://www.cnblogs.com/java-zhao/p/5415896.html (轉aop更改數據源)
https://blog.csdn.net/maoyeqiu/article/details/74011626 (將datasource註入bean簡單方法)
https://blog.csdn.net/neosmith/article/details/61202084(將spring boot自帶的DataSourceAutoConfiguration禁掉)