多数据源是一个比较广泛的需求,如读写库分离、数据散落在多个库等场景中,项目必须具有多数据源访问能力。本文基于工做中一次实现略做探讨,分为:实现原理、实践两大部分。java
目前广泛的JavaWeb项目为Controller-Service-Dao(Mapper)
三层结构,最理想的方案是能在DAO(Mapper)
层方法粒度实现数据源自动切换,这样能最大限度下降耦合,让service
层及以上无感知。并且为了支持动态扩展,使用注解标记是一个比较好的方案。sql
咱们知道Java中访问数据库最底层的标准是JDBC,还记得当初每执行一条sql,都须要写一堆DriveManager.getConnection()、PreparedStatement、ResultSet、close
的处理逻辑吗?
Spring、Mybatis等框架作的事情,就是把全部流程化的代码所有封装,让开发者只需关注Sql、对象处理等业务逻辑,其余流程所有透明化。实现细节此处不展开,但咱们要知道一个结论:Spring帮咱们干了这些活。
要想实现访问数据库的能力,咱们必须配置DataSource
对象。经过它得到数据库链。数据库
//DataSource,其定位就是提供Connectiion package javax.sql; public interface DataSource extends CommonDataSource, Wrapper { Connection getConnection() throws SQLException; Connection getConnection(String username, String password) throws SQLException; }
在Spring框架中,一切Bean都由Spring管理,因此Mybatis在须要DataSource时,会向Spring申请DataSource对象来获取Connection。而Spring也是在这个节点,提供动态切换数据源的能力:AbstractRoutingDataSource
。app
AbstractRoutingDataSource
实现了javax.sql.DataSource
接口,也就是说AbstractRoutingDataSource
自己就是一个数据源,但为什么其具备动态切换功能。而咱们常见的如DruidDataSource
、HikariDataSource
数据源则不具有呢?咱们能够看下其getConnection()方法的实现:框架
//代码有精简 @Override public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); } protected DataSource determineTargetDataSource() { Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); return dataSource; }
经过源码可知,AbstractRoutingDataSource
实际上是个不干活的,其内部维护了一个DataSource
的Map,当外部调用如getConnection
时,其会根据一个Key从自身Map中获取一个数据源,而后把活都交给它干。
此时有两个关键问题:
一、Map中的数据源从何而来?
答案是咱们给它。咱们在配置AbstractRoutingDataSource时,须要将真正的数据源(多个)put进Map中
二、咱们如何告诉Spring,什么时候该用何Key呢?
答案在 determineCurrentLookupKey()
方法。这是一个抽象方法,因此必须由子类实现,此方法会返回查询数据源的Key。固然,要和当初put时key保持一致,否则永远也拿不到DataSource。ide
到目前为止,咱们大体清楚了Spring支持动态数据源切换的实现原理:
一、使用AbstractRoutingDataSource数据源,在其内部经过Map维护咱们要使用的多个真正数据源
二、实现动态返回Key的逻辑post
要使用AbstractRoutingDataSource,咱们需提供一个实现类,实现determineCurrentLookupKey()方法ui
public class MyDynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { //动态返回Key逻辑 }
经过Configuration
类,告诉Spring使用MyDynamicDataSource
首先须要禁用SpringBoot对DataSource的自动配置:@SpringBootApplication(exclude ={DataSourceAutoConfiguration.class})
,而后配置本身的MyDynamicDataSource
:this
//key1,key2,dataSource1,dataSource2 能够经过@Autowired或者其余方式注入 @Bean public DynamicDataSource dynamicDataSource() { DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setDefaultTargetDataSource(默认数据源); Map<Object, Object> dataSourceMap = new HashMap<>(2); dataSourceMap.put(key1, dataSource1); dataSourceMap.put(key2, dataSource2); dataSource.setTargetDataSources(dataSourceMap); return dataSource; }
还须要配置Mybatis框架对象。在SpringBoot中,Mybatis自动配置类为:代理
//重点:@AutoConfigureAfter @AutoConfigureAfter({DataSourceAutoConfiguration.class}) public class MybatisAutoConfiguration { }
咱们禁用了DataSourceAutoConfiguration
配置类,MybatisAutoConfiguration
天然不会生效,Mybatis所须要的SqlSessionFactory
、SqlSessionTemplate
等对象,都须要咱们手动补齐了。
@Bean public SqlSessionFactory(){ }
完成上面的配置后,此时项目已经能运行且能访问数据库。接下来的重点,就是如何正确实现 "动态返回key"的逻辑。
一开始咱们说过,在Mapper的方法粒度,经过注解来控制数据源切换,是个比较好的方案。
经常使用的方案是利用 ThreadLocal来传递key,经过AOP拦截标记了注解的方法,并在调用以前完成key的正确设置。
不过因为Mybatis的实现,Mapper接口的实现类中并无注解信息,这样就致使AOP失效。而如过拦截Mapper全部方法,又会达不到 注解标记的目的。
Mapper层方法注解拦截:Mybatis实现Mapper后,也会向Spring注册对象。咱们能够在此时,利用Map记录全部标记了注解的方法,而后在AOP中拦截全部Mapper方法,而后判断是否命中Map,来实现设置key的目的。
//记录全部须要切换的方法 public class MultipleDataSourceAspect implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { Field mapperInterfaceField = MapperFactoryBean.class.getDeclaredField("mapperInterface"); mapperInterfaceField.setAccessible(true); //获取Mybatis代理的接口 - 遍历方法拿到带切换数据源的方法 - 塞到map中 Class mapperInterfaceClazz = (Class) mapperInterfaceField.get(bean); DataSource classDataSource = (DataSource) mapperInterfaceClazz.getDeclaredAnnotation(DataSource.class); Method[] daoMethods = mapperInterfaceClazz.getDeclaredMethods(); //foreach daoMethods if(method 标记注解){ map.put(Method,key1/key2) } } @Around public Object changeDataSource(ProceedingJoinPoint pjp, Object obj) throws Throwable{ if( pjp.getSignature().getMethod 在Map中){ //切换ThreadLocal中的标记值 } } }