SpringBoot系列博客目錄,含1.5.X版本和2.X版本
springboot2.0正式版發布之后,很多的組件集成需要變更了,這次將多數據源的使用踩的坑給大家填一填。當前多數據源的主要為主從庫,讀寫分離,動態切換數據源。使用的技術就是AOP進行dao方法的切面,所以大家的方法名開頭都需要按照規范進行編寫,如:get***
、add***
等等,
起步基礎
本次的教程需要有springboot2.0集成mybatis 作為基礎:
需要以上的步驟作為基礎,運行成功之后可就可以開始配置多數據源了
開始動手
添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
修改啟動類
修改之前:
@SpringBootApplication
@MapperScan("com.winterchen.dao")
public class SpringBootMybatisMutilDatabaseApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisMutilDatabaseApplication.class, args);
}
}
修改之后:
@SpringBootApplication
public class SpringBootMybatisMutilDatabaseApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisMutilDatabaseApplication.class, args);
}
}
因為改用多數據源,所以dao接口的掃描我們放在配置類中進行
修改項目配置
首先我們需要在配置文件中配置多數據源,看一下原本項目的配置:
spring:
datasource:
name: mysql_test
#-----------------start-----------------# (1)
type: com.alibaba.druid.pool.DruidDataSource
#-----------------end-----------------#
#druid相關配置
druid:
#監控統計攔截的filters
filters: stat
#-----------------start-----------------# (2)
driver-class-name: com.mysql.jdbc.Driver
#基本屬性
url: jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
username: root
password: root
#-----------------end-----------------#
#配置初始化大小/最小/最大
initial-size: 1
min-idle: 1
max-active: 20
#獲取連接等待超時時間
max-wait: 60000
#間隔多久進行一次檢測,檢測需要關閉的空閑連接
time-between-eviction-runs-millis: 60000
#一個連接在池中最小生存的時間
min-evictable-idle-time-millis: 300000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
#打開PSCache,並指定每個連接上PSCache的大小。oracle設為true,mysql設為false。分庫分表較多推薦設置為false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
**需要修改的地方: **
-
(1) 需要將
type: com.alibaba.druid.pool.DruidDataSource
去除; -
(2) 將關於數據庫的連接信息:
driver-class-name
、url
、username
、password
去除;
修改后:
spring:
datasource:
name: mysql_test
#-------------- start ----------------# (1)
master:
#基本屬性--注意,這里的為【jdbcurl】-- 默認使用HikariPool作為數據庫連接池
jdbcurl: jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
slave:
#基本屬性--注意,這里為 【url】-- 使用 druid 作為數據庫連接池
url: jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
read: get,select,count,list,query,find
write: add,create,update,delete,remove,insert
#-------------- end ----------------#
#druid相關配置
druid:
#監控統計攔截的filters
filters: stat,wall
#配置初始化大小/最小/最大
initial-size: 1
min-idle: 1
max-active: 20
#獲取連接等待超時時間
max-wait: 60000
#間隔多久進行一次檢測,檢測需要關閉的空閑連接
time-between-eviction-runs-millis: 60000
#一個連接在池中最小生存的時間
min-evictable-idle-time-millis: 300000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
#打開PSCache,並指定每個連接上PSCache的大小。oracle設為true,mysql設為false。分庫分表較多推薦設置為false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
需要修改地方:
- (1) 在如上的配置中添加
master
、slave
兩個數據源;
注意!!兩中數據源中有一處是不一樣的,原因是因為master
數據源使用 Hikari
連接池,slave
使用的是druid
作為數據庫連接池,所以兩處的配置分別為:
master:
jdbcurl: jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
slave:
url: jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
數據庫的連接不一樣的,如果配置成一樣的會在啟動的時候報錯。
注意!!
dao接口方法的方法名規則配置在這里了,當然可以自行更改:
read: get,select,count,list,query,find
write: add,create,update,delete,remove,insert
創建配置包
首先在項目的/src/main/java/com/winterchen/
包下創建config
包
創建數據源類型的枚舉DatabaseType
該枚舉類主要用來區分讀寫
package com.winterchen.config;
/**
* 列出數據源類型
* Created by Donghua.Chen on 2018/5/29.
*/
public enum DatabaseType {
master("write"), slave("read");
DatabaseType(String name) {
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "DatabaseType{" +
"name='" + name + '\'' +
'}';
}
}
創建線程安全的DatabaseType容器
多數據源必須要保證數據源的線程安全的
package com.winterchen.config;
/**
* 保存一個線程安全的DatabaseType容器
* Created by Donghua.Chen on 2018/5/29.
*/
public class DatabaseContextHolder {
//用於存放多線程環境下的成員變量
private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>();
public static void setDatabaseType(DatabaseType type) {
contextHolder.set(type);
}
public static DatabaseType getDatabaseType() {
return contextHolder.get();
}
}
創建動態數據源
實現數據源切換的功能就是自定義一個類擴展AbstractRoutingDataSource抽象類,其實該相當於數據源DataSource的路由中介,可以實現在項目運行時根據相應key值切換到對應的數據源DataSource上,有興趣的同學可以看看它的源碼。
public class DynamicDataSource extends AbstractRoutingDataSource {
static final Map<DatabaseType, List<String>> METHOD_TYPE_MAP = new HashMap<>();
@Nullable
@Override
protected Object determineCurrentLookupKey() {
DatabaseType type = DatabaseContextHolder.getDatabaseType();
logger.info("====================dataSource ==========" + type);
return type;
}
void setMethodType(DatabaseType type, String content) {
List<String> list = Arrays.asList(content.split(","));
METHOD_TYPE_MAP.put(type, list);
}
}
創建數據源配置類DataSourceConfig
@Configuration
@MapperScan("com.winterchen.dao")
@EnableTransactionManagement
public class DataSourceConfig {
private static Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);
@Autowired
private Environment env; // (1)
@Autowired
private DataSourceProperties properties; // (2)
@Value("${spring.datasource.druid.filters}") // (3)
private String filters;
@Value("${spring.datasource.druid.initial-size}")
private Integer initialSize;
@Value("${spring.datasource.druid.min-idle}")
private Integer minIdle;
@Value("${spring.datasource.druid.max-active}")
private Integer maxActive;
@Value("${spring.datasource.druid.max-wait}")
private Integer maxWait;
@Value("${spring.datasource.druid.time-between-eviction-runs-millis}")
private Long timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.druid.min-evictable-idle-time-millis}")
private Long minEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.validation-query}")
private String validationQuery;
@Value("${spring.datasource.druid.test-while-idle}")
private Boolean testWhileIdle;
@Value("${spring.datasource.druid.test-on-borrow}")
private boolean testOnBorrow;
@Value("${spring.datasource.druid.test-on-return}")
private boolean testOnReturn;
@Value("${spring.datasource.druid.pool-prepared-statements}")
private boolean poolPreparedStatements;
@Value("${spring.datasource.druid.max-pool-prepared-statement-per-connection-size}")
private Integer maxPoolPreparedStatementPerConnectionSize;
/**
* 通過Spring JDBC 快速創建 DataSource
* @return
*/
@Bean(name = "masterDataSource")
@Qualifier("masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master") // (4)
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 手動創建DruidDataSource,通過DataSourceProperties 讀取配置
* @return
* @throws SQLException
*/
@Bean(name = "slaveDataSource")
@Qualifier("slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() throws SQLException {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setFilters(filters);
dataSource.setUrl(properties.getUrl());
dataSource.setDriverClassName(properties.getDriverClassName());
dataSource.setUsername(properties.getUsername());
dataSource.setPassword(properties.getPassword());
dataSource.setInitialSize(initialSize);
dataSource.setMinIdle(minIdle);
dataSource.setMaxActive(maxActive);
dataSource.setMaxWait(maxWait);
dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
dataSource.setValidationQuery(validationQuery);
dataSource.setTestWhileIdle(testWhileIdle);
dataSource.setTestOnBorrow(testOnBorrow);
dataSource.setTestOnReturn(testOnReturn);
dataSource.setPoolPreparedStatements(poolPreparedStatements);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
return dataSource;
}
/**
* 構造多數據源連接池
* Master 數據源連接池采用 HikariDataSource
* Slave 數據源連接池采用 DruidDataSource
* @param master
* @param slave
* @return
*/
@Bean
@Primary
public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource master,
@Qualifier("slaveDataSource") DataSource slave) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DatabaseType.master, master);
targetDataSources.put(DatabaseType.slave, slave);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);// 該方法是AbstractRoutingDataSource的方法
dataSource.setDefaultTargetDataSource(slave);// 默認的datasource設置為myTestDbDataSource
String read = env.getProperty("spring.datasource.read");
dataSource.setMethodType(DatabaseType.slave, read);
String write = env.getProperty("spring.datasource.write");
dataSource.setMethodType(DatabaseType.master, write);
return dataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource myTestDbDataSource,
@Qualifier("slaveDataSource") DataSource myTestDb2DataSource) throws Exception {
SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
fb.setDataSource(this.dataSource(myTestDbDataSource, myTestDb2DataSource));
fb.setTypeAliasesPackage(env.getProperty("mybatis.type-aliases-package"));
fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapper-locations")));
return fb.getObject();
}
@Bean
public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
return new DataSourceTransactionManager(dataSource);
}
}
以上的代碼中:
-
(1) 注入類
Environment
可以很方便的獲取配置文件中的參數 -
(2)
DataSourceProperties
和(4)中的@ConfigurationProperties(prefix = "spring.datasource.master")
配合使用,將配置文件中的配置數據自動封裝到實體類DataSourceProperties
中 -
(3)
@Value
注解同樣是指定獲取配置文件中的配置;
更詳細的配置大家可以參考官方文檔。
配置AOP
本章的開頭已經說過,多數據源動態切換的原理是利用AOP切面進行動態的切換的,當調用dao
接口方法時,根據接口方法的方法名開頭進行區分讀寫。
/**
*
* 動態處理數據源,根據命名區分
* Created by Donghua.Chen on 2018/5/29.
*/
@Aspect
@Component
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DataSourceAspect {
private static Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
@Pointcut("execution(* com.winterchen.dao.*.*(..))")//切點
public void aspect() {
}
@Before("aspect()")
public void before(JoinPoint point) { //在指定切點的方法之前執行
String className = point.getTarget().getClass().getName();
String method = point.getSignature().getName();
String args = StringUtils.join(point.getArgs(), ",");
logger.info("className:{}, method:{}, args:{} ", className, method, args);
try {
for (DatabaseType type : DatabaseType.values()) {
List<String> values = DynamicDataSource.METHOD_TYPE_MAP.get(type);
for (String key : values) {
if (method.startsWith(key)) {
logger.info(">>{} 方法使用的數據源為:{}<<", method, key);
DatabaseContextHolder.setDatabaseType(type);
DatabaseType types = DatabaseContextHolder.getDatabaseType();
logger.info(">>{}方法使用的數據源為:{}<<", method, types);
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
如上可以看到,切點切在dao
的接口方法中,根據接口方法的方法名進行匹配數據源,然后將數據源set到用於存放數據源線程安全的容器中;
完整的項目結構了解一下:
項目啟動
啟動成功:
2018-05-30 17:27:16.492 INFO 35406 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located MBean 'masterDataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=masterDataSource,type=HikariDataSource]
2018-05-30 17:27:16.496 INFO 35406 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located MBean 'slaveDataSource': registering with JMX server as MBean [com.alibaba.druid.pool:name=slaveDataSource,type=DruidDataSource]
2018-05-30 17:27:16.498 INFO 35406 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located MBean 'statFilter': registering with JMX server as MBean [com.alibaba.druid.filter.stat:name=statFilter,type=StatFilter]
2018-05-30 17:27:16.590 INFO 35406 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2018-05-30 17:27:16.598 INFO 35406 --- [ main] pringBootMybatisMutilDatabaseApplication : Started SpringBootMybatisMutilDatabaseApplication in 11.523 seconds (JVM running for 13.406)
添加用戶(write):
日志:
2018-05-30 17:29:07.347 INFO 35406 --- [nio-8080-exec-1] com.winterchen.config.DataSourceAspect : className:com.sun.proxy.$Proxy73, method:insert, args:com.winterchen.model.UserDomain@4b5b52dc
2018-05-30 17:29:07.350 INFO 35406 --- [nio-8080-exec-1] com.winterchen.config.DataSourceAspect : >>insert 方法使用的數據源為:insert<<
2018-05-30 17:29:07.351 INFO 35406 --- [nio-8080-exec-1] com.winterchen.config.DataSourceAspect : >>insert方法使用的數據源為:DatabaseType{name='write'}<<
2018-05-30 17:29:07.461 INFO 35406 --- [nio-8080-exec-1] com.winterchen.config.DynamicDataSource : ====================dataSource ==========DatabaseType{name='write'}
2018-05-30 17:29:07.462 INFO 35406 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2018-05-30 17:29:07.952 INFO 35406 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
可以看出使用的就是write
數據源,並且該數據源是使用HikariPool
作為數據庫連接池的
查詢用戶(read):
日志:
2018-05-30 17:29:41.616 INFO 35406 --- [nio-8080-exec-2] com.winterchen.config.DataSourceAspect : className:com.sun.proxy.$Proxy73, method:selectUsers, args:
2018-05-30 17:29:41.618 INFO 35406 --- [nio-8080-exec-2] com.winterchen.config.DataSourceAspect : >>selectUsers 方法使用的數據源為:select<<
2018-05-30 17:29:41.618 INFO 35406 --- [nio-8080-exec-2] com.winterchen.config.DataSourceAspect : >>selectUsers方法使用的數據源為:DatabaseType{name='read'}<<
2018-05-30 17:29:41.693 INFO 35406 --- [nio-8080-exec-2] com.winterchen.config.DynamicDataSource : ====================dataSource ==========DatabaseType{name='read'}
2018-05-30 17:29:41.982 INFO 35406 --- [nio-8080-exec-2] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
可以看出使用的是read
數據源。
源碼地址:戳這里