springboot动态数据源实现的两个关键要素
说起动态数据源,大家肯定都觉得是个很神秘的东西,其实若仔细研究就会发现并不复杂,具体来说,只有两步: 第一、spring容器中注入两个数据源。 第二、在运行时根据业务需要获取特定的数据源。
读者老爷:老哥你这不是废话吗,看完你这个讲解,发现它就是讲解,屁用没有。 作者老弟:别急麻,现实中咱们解决问题就是,从外到里,一层一层递进。咱们就从第一步spring容器注入多数据源开始递进。 首先咱们要搞明白的,如何注入数据源及注入的数据源由谁来管理呢? 注入数据源我就不讲解了就是声明几个bean,关键是这个多个数据源由谁管理,具体的是哪个类来管理?
讲这个之前,咱们梳理一下平常使用spring+mybatis的操作数据库的流程,基本上就是如下四步。
datasource–>sqlSessionfactory–>connnection–>crud。
大家仔细考虑,sqlsessionFactory会关心你用什么数据源吗?显然它不会,只要你给一个它能用的数据源,他就能帮你干事。所以咱们要实现动态数据源就得在dataSource这里做文章。 那怎么做呢? 试想一下,如果咱们有一个自定义的包装类,它继承了DataSource,并且咱们在这个包装类内部维护一个存储多个数据源的MAP,当调用的时候咱们根据业务逻辑传过来的key,动态获取对应的dataSource不就实现这个功能了吗。你看动态数据源它本质就是这么个原理。
同时spring这个框架老哥其实已经帮我们实现了这个包装类,org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource AbstractRoutingDataSource是一个抽象类,该类中determineTargetDataSource()方法将调用determineCurrentLookupKey()方法来动态获取数据源, 我们要做的就是写一个子类继承该类,然后重写里面的determineCurrentLookupKey()方法,用于自定义key的获取方式,一般的我们将数据源的key存放在ThreadLocal中。定义好子类之后,咱们就将声明好的多个数据源配置到该子类中,然后将该类注入到spring容器中,同时交给sqlSessionFactory。
自定义DynamicDataSource类,包含两块:
1、重写determineCurrentLookupKey()方法,用于从当前线程中获取对应的数据源。 2、CONTEXT_HOLDER 一个ThreaLocal用于数据源与当前线程的绑定与解绑。
package com.app.microservice.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.ArrayDeque;
import java.util.Deque;
public class DynamicDatasource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return peek();
}
private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = new ThreadLocal() {
@Override
protected Object initialValue() {
return new ArrayDeque();
}
};
public static String peek() {
return CONTEXT_HOLDER.get().peek();
}
public static void push(String dataSource) {
CONTEXT_HOLDER.get().push(dataSource);
}
public static void poll() {
Deque<String> deque = CONTEXT_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
CONTEXT_HOLDER.remove();
}
}
}
动态数据源配置类 DynamicDataSourceConfig,,这里为了演示方便,我只是做了个样例,里面具体数据源的相关配置(primary、slave)以及sqlSessionFactory,还需要大家自己定义。
package com.app.microservice.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.session.defaults.DefaultSqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Configuration
public class DynamicDataSourceConfig {
@Primary
@Bean("primaryDatasource")
public DruidDataSource primary(){
DruidDataSource primary = new DruidDataSource();
return primary;
}
@Bean("slaveDatasource")
public DruidDataSource slave(){
DruidDataSource slave = new DruidDataSource();
return slave;
}
@Bean
public DynamicDatasource dynamicDatasource(@Qualifier("primaryDatasource") DruidDataSource primary,
@Qualifier("slaveDatasource") DruidDataSource slave){
DynamicDatasource dynamicDatasource = new DynamicDatasource();
Map<Object, Object> targetDataSources = new ConcurrentHashMap<>();
targetDataSources.put("primaryDatasource",primary);
targetDataSources.put("slaveDatasource",slave);
dynamicDatasource.setTargetDataSources(targetDataSources);
dynamicDatasource.setDefaultTargetDataSource(primary);
dynamicDatasource.afterPropertiesSet();
return dynamicDatasource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DynamicDatasource dynamicDatasource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDatasource);
return sqlSessionFactoryBean.getObject();
}
}
以上咱们就做好了动态数据源的配置,那么接下来就是在业务代码中进行数据源的动态切换。
其实现也不复杂,具体的就是使用AOP技术,这里我们可以自定义一个注解,然后在需要用到的业务逻辑上标注该注解,并在注解参数中声明使用数据源的key,然后咱们使用AOP技术对标有该注解的方法进行拦截,然后切换到指定的数据源即可。
注解+AOP实现数据源切换
package com.app.microservice.config;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Order(1)
@Component
public class DataSourceAspect{
@Pointcut("@annotation(com.app.microservice.config.DataSourceAnnotation)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSourceAnnotation dataSourceAnnotation = method.getAnnotation(DataSourceAnnotation.class);
if (dataSourceAnnotation != null) {
DynamicDatasource.push(dataSourceAnnotation.value().name());
}
try {
return point.proceed();
} finally {
DynamicDatasource.poll();
}
}
}
好啦,大功告成!!!!
总结
不知到,有没有人困惑,在讲解org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource动态数据源原理时,我说determineTargetDataSource()方法将调用determineCurrentLookupKey()方法来动态获取数据源 但是determineTargetDataSource()这个方法是谁调用的呢? 其实这个问题困惑了我,虽然知道最终这个会由持久层框架mybatis来获取真正的数据源,但是mybatis里面应该不会直接是用determineTargetDataSource()这个方法来获取数据源呀,如果真是这样那耦合也太大了,,后来我在翻看AbstractRoutingDataSource这个类发现了这个两个方法,unwrap()和isWrapperFor(),发现它在这里调用了determineTargetDataSource(),并且unwrap其实是DataSource接口继承而来,,,那么这个问题就清楚了,,持久成框架获取的肯定是DataSource类型的,所以它不用关心子类到底是谁,只要你这个子类实现了DataSource接口,那么它就可以去包装获取正真的数据源。看到此我真正感受它的巧妙以及面向接口编程的强大。
|