springboot集成多数据源
简要原理:
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 public SqlSessionFactory sqlSessionFactory(@Qualifier("writeDS") DataSource writeDS, @Qualifier("readDS") DataSource readDS) throws Exception{ SqlSessionFactoryBean fb = new SqlSessionFactoryBean(); fb.setDataSource(this.dataSource(writeDS, readDS)); fb.setTypeAliasesPackage(basicPackage); fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocation)); return fb.getObject(); }
主配置文件
#配置使用的文件 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禁掉)