SpringBoot之Mybatis多数据源配置
首先,能接触到多数据源的必定已经对多数据源有一定的了解了。目前常用的多数据源配置方式一共分为两种:
一种是多数据源与单数据源配置方式无异的做法,只是将mapper区分开来就可以。但是这种方式有他不可避免的弊端。如果两个数据源最终要使用同一套mapper.xml,mapper.java,或者业务逻辑。这种做法是不能够做到的。你可能需要重新copy一份一摸一样的mapper来供新数据源使用。
所以,为了尽可能的提高代码的重用,我们有了第二种方式:动态设置数据源。
tips:此前已经有很多前辈对此做了很多说明,我这里只是对工作中遇到的实际问题进行一个记录总结。keywords:SpringBoot多数据源、AOP切换数据源、AbstractRoutingDataSource等
动态设置数据源原理:首先我们要知道,动态最终体现在哪里是动态的。通过Spring-jdbc提供的抽象类AbstractRoutingDataSource我们可以明确的知道,最终变化的是Connection。而且我们可以通过继承这个抽象类很容易的实现一个动态数据源的切换。
现在,有了最基本的切换工具,我们要结合自己的业务逻辑来进行合理的修饰。使之尽可能符合自己的业务。
这里我说一下我遇到的问题,ABCD四个数据库,CD结构完全一致,需要从CD种检索数据,并插入A。所以,整个过程种至少会有两个数据源之间的转换。这里通过注解+AOP的方式处理。
1.多个数据源定义
这四个基本内容如下:当然你也可以直接定义在一个文件中
@Configuration public class MybatisConfig { private Logger logger = LoggerFactory.getLogger(getClass()); @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSourceProperties loanDataSourceProperties(){ return new DataSourceProperties(); } @Bean(name = "loanDataSource") public DataSource dataSource() throws Exception { return DruidDataSourceFactory.createDataSource(DataSourceUtil.buildDataSourceParam(loanDataSourceProperties())); } }
既然是多数据源,那么我们就需要有一个bean来将之前定义的所有单个数据源组合起来。
@Configuration public class MultipleDataSourceConfig extends ApplicationObjectSupport { @Primary @Bean(name = "dynamicDataSource") public DataSource dynamicDataSource(){ DynamicDataSource dynamicDataSource = new DynamicDataSource(); ApplicationContext applicationContext = getApplicationContext(); Map dataSources = applicationContext.getBeansOfType(DataSource.class); dynamicDataSource.setTargetDataSources(dataSources); return dynamicDataSource; } @Bean(name = "loanSessionFactory") @Primary public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactory sqlSessionFactory = null; try { sqlSessionFactory = DataSourceUtil.getSessionFactory(dynamicDataSource(),"classpath:/mapper/**/*Mapper.xml"); } catch (Exception e) { logger.error("fail to init SqlSessionFactory", e); } return sqlSessionFactory; } }
2.继承AbstractRoutingDataSource来实现动态切换的逻辑
public class DynamicDataSource extends AbstractRoutingDataSource { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Override protected Object determineCurrentLookupKey() { String dataSourceName = null; if(DataSourceContextholder.getDataSource() == null || DataSourceContextholder.getDataSource().empty()){ dataSourceName = DataSourceContextholder.DEFAULT_DATASOURCE; }else { dataSourceName = DataSourceContextholder.getDataSource().peek(); } logger.info("current dataSource is {}",dataSourceName); return dataSourceName; } }
这里的determineCurrentLookupKey方法,通常只需要返回一个字符串来通知当前使用的数据源是那个。
3.用来保存当前数据源的上下文环境ThreadLocal:
public class DataSourceContextholder { public static final String DEFAULT_DATASOURCE = "loanDataSource"; private static ThreadLocal<Stack<String>> contextHolder = new ThreadLocal<>(); //设置数据源名字 public static void setDataSource(Stack<String> dataSource){ contextHolder.set(dataSource); } //获取数据源名 public static Stack<String> getDataSource(){ return contextHolder.get(); } //清除数据源名 public static void clearDataSource(){ contextHolder.get().pop(); if(contextHolder.get().empty()){ contextHolder.remove(); } } }
解释一下,这里为什么使用Stack,由于历史代码原因,或者业务逻辑,使得数据源存在嵌套关系。
4.数据源注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DataSource { String value() default "";//数据源名称 boolean flowParent() default false; //是否使用当前栈中的第一个数据源作为当前数据源 }
5.AOP,动态改变栈中数据
@Aspect @Component @Order(-1) public class DataSourceChange { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Pointcut("@annotation(dataSource)") public void pointCut(DataSource dataSource){} @Before("pointCut(dataSource)") public void changeDataSource(DataSource dataSource){ String value = dataSource.value(); boolean flowParent = dataSource.flowParent(); Stack<String> source = DataSourceContextholder.getDataSource(); String dataSourceName = StringUtils.isBlank(value) ? DataSourceContextholder.DEFAULT_DATASOURCE : value; if(source == null || source.empty()){ Stack<String> dataSources = new Stack<>(); dataSources.push(dataSourceName); DataSourceContextholder.setDataSource(dataSources); }else if(StringUtils.isBlank(value) && !source.empty() && !flowParent){ source.push(source.peek()); }else if(flowParent && !source.empty()){ String parentDataSource = source.elementAt(0); source.push(parentDataSource); }else { source.push(value); } } @After("pointCut(dataSource)") public void clearDataSource(DataSource dataSource){ Stack<String> temp = DataSourceContextholder.getDataSource(); if(temp != null){ DataSourceContextholder.clearDataSource(); } } }
文中没有提到的各种配置项,与常规配置无异。
2018-11-7 记