简要原理:
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禁掉)